Merge branch 'main' into instrument-keyboard-events
This commit is contained in:
commit
b26a468820
188 changed files with 3391 additions and 1997 deletions
83
Cargo.lock
generated
83
Cargo.lock
generated
|
@ -292,6 +292,15 @@ version = "1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16"
|
checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "assets"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"gpui",
|
||||||
|
"rust-embed",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "assistant"
|
name = "assistant"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
@ -1443,7 +1452,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "collab"
|
name = "collab"
|
||||||
version = "0.34.0"
|
version = "0.36.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
|
@ -1464,6 +1473,7 @@ dependencies = [
|
||||||
"editor",
|
"editor",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"envy",
|
"envy",
|
||||||
|
"file_finder",
|
||||||
"fs",
|
"fs",
|
||||||
"futures 0.3.28",
|
"futures 0.3.28",
|
||||||
"git",
|
"git",
|
||||||
|
@ -1477,6 +1487,7 @@ dependencies = [
|
||||||
"live_kit_server",
|
"live_kit_server",
|
||||||
"log",
|
"log",
|
||||||
"lsp",
|
"lsp",
|
||||||
|
"menu",
|
||||||
"nanoid",
|
"nanoid",
|
||||||
"node_runtime",
|
"node_runtime",
|
||||||
"notifications",
|
"notifications",
|
||||||
|
@ -1550,6 +1561,7 @@ dependencies = [
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"settings",
|
"settings",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
|
"story",
|
||||||
"theme",
|
"theme",
|
||||||
"theme_selector",
|
"theme_selector",
|
||||||
"time",
|
"time",
|
||||||
|
@ -3428,6 +3440,40 @@ dependencies = [
|
||||||
"tiff",
|
"tiff",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "include-flate"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c2e11569346406931d20276cc460215ee2826e7cad43aa986999cb244dd7adb0"
|
||||||
|
dependencies = [
|
||||||
|
"include-flate-codegen-exports",
|
||||||
|
"lazy_static",
|
||||||
|
"libflate",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "include-flate-codegen"
|
||||||
|
version = "0.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3a7d6e1419fa3129eb0802b4c99603c0d425c79fb5d76191d5a20d0ab0d664e8"
|
||||||
|
dependencies = [
|
||||||
|
"libflate",
|
||||||
|
"proc-macro-hack",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 1.0.109",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "include-flate-codegen-exports"
|
||||||
|
version = "0.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "75657043ffe3d8280f1cb8aef0f505532b392ed7758e0baeac22edadcee31a03"
|
||||||
|
dependencies = [
|
||||||
|
"include-flate-codegen",
|
||||||
|
"proc-macro-hack",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "1.9.3"
|
version = "1.9.3"
|
||||||
|
@ -3819,6 +3865,26 @@ version = "0.2.148"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b"
|
checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libflate"
|
||||||
|
version = "1.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5ff4ae71b685bbad2f2f391fe74f6b7659a34871c08b210fdc039e43bee07d18"
|
||||||
|
dependencies = [
|
||||||
|
"adler32",
|
||||||
|
"crc32fast",
|
||||||
|
"libflate_lz77",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libflate_lz77"
|
||||||
|
version = "1.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a52d3a8bfc85f250440e4424db7d857e241a3aebbbe301f3eb606ab15c39acbf"
|
||||||
|
dependencies = [
|
||||||
|
"rle-decode-fast",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libgit2-sys"
|
name = "libgit2-sys"
|
||||||
version = "0.14.2+1.5.1"
|
version = "0.14.2+1.5.1"
|
||||||
|
@ -5396,6 +5462,12 @@ dependencies = [
|
||||||
"version_check",
|
"version_check",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro-hack"
|
||||||
|
version = "0.5.20+deprecated"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.67"
|
version = "1.0.67"
|
||||||
|
@ -6090,6 +6162,12 @@ dependencies = [
|
||||||
"syn 1.0.109",
|
"syn 1.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rle-decode-fast"
|
||||||
|
version = "1.0.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3582f63211428f83597b51b2ddb88e2a91a9d52d12831f9d08f5e624e8977422"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rmp"
|
name = "rmp"
|
||||||
version = "0.8.12"
|
version = "0.8.12"
|
||||||
|
@ -6237,6 +6315,7 @@ version = "8.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b1e7d90385b59f0a6bf3d3b757f3ca4ece2048265d70db20a2016043d4509a40"
|
checksum = "b1e7d90385b59f0a6bf3d3b757f3ca4ece2048265d70db20a2016043d4509a40"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"include-flate",
|
||||||
"rust-embed-impl",
|
"rust-embed-impl",
|
||||||
"rust-embed-utils",
|
"rust-embed-utils",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
|
@ -7438,6 +7517,7 @@ dependencies = [
|
||||||
"backtrace-on-stack-overflow",
|
"backtrace-on-stack-overflow",
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap 4.4.4",
|
"clap 4.4.4",
|
||||||
|
"collab_ui",
|
||||||
"dialoguer",
|
"dialoguer",
|
||||||
"editor",
|
"editor",
|
||||||
"fuzzy",
|
"fuzzy",
|
||||||
|
@ -9529,6 +9609,7 @@ dependencies = [
|
||||||
"activity_indicator",
|
"activity_indicator",
|
||||||
"ai",
|
"ai",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"assets",
|
||||||
"assistant",
|
"assistant",
|
||||||
"async-compression",
|
"async-compression",
|
||||||
"async-recursion 0.3.2",
|
"async-recursion 0.3.2",
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = [
|
||||||
|
"crates/assets",
|
||||||
"crates/activity_indicator",
|
"crates/activity_indicator",
|
||||||
"crates/ai",
|
"crates/ai",
|
||||||
"crates/assistant",
|
"crates/assistant",
|
||||||
|
@ -109,7 +110,7 @@ prost = { version = "0.8" }
|
||||||
rand = { version = "0.8.5" }
|
rand = { version = "0.8.5" }
|
||||||
refineable = { path = "./crates/refineable" }
|
refineable = { path = "./crates/refineable" }
|
||||||
regex = { version = "1.5" }
|
regex = { version = "1.5" }
|
||||||
rust-embed = { version = "8.0", features = ["include-exclude"] }
|
rust-embed = { version = "8.0", features = ["include-exclude", "compression"] }
|
||||||
rusqlite = { version = "0.29.0", features = ["blob", "array", "modern_sqlite"] }
|
rusqlite = { version = "0.29.0", features = ["blob", "array", "modern_sqlite"] }
|
||||||
schemars = { version = "0.8" }
|
schemars = { version = "0.8" }
|
||||||
serde = { version = "1.0", features = ["derive", "rc"] }
|
serde = { version = "1.0", features = ["derive", "rc"] }
|
||||||
|
|
|
@ -77,9 +77,6 @@ impl ActivityIndicator {
|
||||||
cx.observe(auto_updater, |_, _, cx| cx.notify()).detach();
|
cx.observe(auto_updater, |_, _, cx| cx.notify()).detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
// cx.observe_active_labeled_tasks(|_, cx| cx.notify())
|
|
||||||
// .detach();
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
statuses: Default::default(),
|
statuses: Default::default(),
|
||||||
project: project.clone(),
|
project: project.clone(),
|
||||||
|
@ -288,15 +285,6 @@ impl ActivityIndicator {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// todo!(show active tasks)
|
|
||||||
// if let Some(most_recent_active_task) = cx.active_labeled_tasks().last() {
|
|
||||||
// return Content {
|
|
||||||
// icon: None,
|
|
||||||
// message: most_recent_active_task.to_string(),
|
|
||||||
// on_click: None,
|
|
||||||
// };
|
|
||||||
// }
|
|
||||||
|
|
||||||
Default::default()
|
Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
12
crates/assets/Cargo.toml
Normal file
12
crates/assets/Cargo.toml
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
[package]
|
||||||
|
name = "assets"
|
||||||
|
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]
|
||||||
|
gpui = {path = "../gpui"}
|
||||||
|
rust-embed.workspace = true
|
||||||
|
anyhow.workspace = true
|
|
@ -1,3 +1,4 @@
|
||||||
|
// This crate was essentially pulled out verbatim from main `zed` crate to avoid having to run RustEmbed macro whenever zed has to be rebuilt. It saves a second or two on an incremental build.
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
|
|
||||||
use gpui::{AssetSource, Result, SharedString};
|
use gpui::{AssetSource, Result, SharedString};
|
|
@ -933,7 +933,7 @@ impl AssistantPanel {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_hamburger_button(cx: &mut ViewContext<Self>) -> impl IntoElement {
|
fn render_hamburger_button(cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
IconButton::new("hamburger_button", Icon::Menu)
|
IconButton::new("hamburger_button", IconName::Menu)
|
||||||
.on_click(cx.listener(|this, _event, cx| {
|
.on_click(cx.listener(|this, _event, cx| {
|
||||||
if this.active_editor().is_some() {
|
if this.active_editor().is_some() {
|
||||||
this.set_active_editor_index(None, cx);
|
this.set_active_editor_index(None, cx);
|
||||||
|
@ -957,7 +957,7 @@ impl AssistantPanel {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_split_button(cx: &mut ViewContext<Self>) -> impl IntoElement {
|
fn render_split_button(cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
IconButton::new("split_button", Icon::Snip)
|
IconButton::new("split_button", IconName::Snip)
|
||||||
.on_click(cx.listener(|this, _event, cx| {
|
.on_click(cx.listener(|this, _event, cx| {
|
||||||
if let Some(active_editor) = this.active_editor() {
|
if let Some(active_editor) = this.active_editor() {
|
||||||
active_editor.update(cx, |editor, cx| editor.split(&Default::default(), cx));
|
active_editor.update(cx, |editor, cx| editor.split(&Default::default(), cx));
|
||||||
|
@ -968,7 +968,7 @@ impl AssistantPanel {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_assist_button(cx: &mut ViewContext<Self>) -> impl IntoElement {
|
fn render_assist_button(cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
IconButton::new("assist_button", Icon::MagicWand)
|
IconButton::new("assist_button", IconName::MagicWand)
|
||||||
.on_click(cx.listener(|this, _event, cx| {
|
.on_click(cx.listener(|this, _event, cx| {
|
||||||
if let Some(active_editor) = this.active_editor() {
|
if let Some(active_editor) = this.active_editor() {
|
||||||
active_editor.update(cx, |editor, cx| editor.assist(&Default::default(), cx));
|
active_editor.update(cx, |editor, cx| editor.assist(&Default::default(), cx));
|
||||||
|
@ -979,7 +979,7 @@ impl AssistantPanel {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_quote_button(cx: &mut ViewContext<Self>) -> impl IntoElement {
|
fn render_quote_button(cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
IconButton::new("quote_button", Icon::Quote)
|
IconButton::new("quote_button", IconName::Quote)
|
||||||
.on_click(cx.listener(|this, _event, cx| {
|
.on_click(cx.listener(|this, _event, cx| {
|
||||||
if let Some(workspace) = this.workspace.upgrade() {
|
if let Some(workspace) = this.workspace.upgrade() {
|
||||||
cx.window_context().defer(move |cx| {
|
cx.window_context().defer(move |cx| {
|
||||||
|
@ -994,7 +994,7 @@ impl AssistantPanel {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_plus_button(cx: &mut ViewContext<Self>) -> impl IntoElement {
|
fn render_plus_button(cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
IconButton::new("plus_button", Icon::Plus)
|
IconButton::new("plus_button", IconName::Plus)
|
||||||
.on_click(cx.listener(|this, _event, cx| {
|
.on_click(cx.listener(|this, _event, cx| {
|
||||||
this.new_conversation(cx);
|
this.new_conversation(cx);
|
||||||
}))
|
}))
|
||||||
|
@ -1004,12 +1004,12 @@ impl AssistantPanel {
|
||||||
|
|
||||||
fn render_zoom_button(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
fn render_zoom_button(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
let zoomed = self.zoomed;
|
let zoomed = self.zoomed;
|
||||||
IconButton::new("zoom_button", Icon::Maximize)
|
IconButton::new("zoom_button", IconName::Maximize)
|
||||||
.on_click(cx.listener(|this, _event, cx| {
|
.on_click(cx.listener(|this, _event, cx| {
|
||||||
this.toggle_zoom(&ToggleZoom, cx);
|
this.toggle_zoom(&ToggleZoom, cx);
|
||||||
}))
|
}))
|
||||||
.selected(zoomed)
|
.selected(zoomed)
|
||||||
.selected_icon(Icon::Minimize)
|
.selected_icon(IconName::Minimize)
|
||||||
.icon_size(IconSize::Small)
|
.icon_size(IconSize::Small)
|
||||||
.tooltip(move |cx| {
|
.tooltip(move |cx| {
|
||||||
Tooltip::for_action(if zoomed { "Zoom Out" } else { "Zoom In" }, &ToggleZoom, cx)
|
Tooltip::for_action(if zoomed { "Zoom Out" } else { "Zoom In" }, &ToggleZoom, cx)
|
||||||
|
@ -1286,8 +1286,8 @@ impl Panel for AssistantPanel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn icon(&self, cx: &WindowContext) -> Option<Icon> {
|
fn icon(&self, cx: &WindowContext) -> Option<IconName> {
|
||||||
Some(Icon::Ai).filter(|_| AssistantSettings::get_global(cx).button)
|
Some(IconName::Ai).filter(|_| AssistantSettings::get_global(cx).button)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
|
fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
|
||||||
|
@ -2349,7 +2349,7 @@ impl ConversationEditor {
|
||||||
div()
|
div()
|
||||||
.id("error")
|
.id("error")
|
||||||
.tooltip(move |cx| Tooltip::text(error.clone(), cx))
|
.tooltip(move |cx| Tooltip::text(error.clone(), cx))
|
||||||
.child(IconElement::new(Icon::XCircle)),
|
.child(Icon::new(IconName::XCircle)),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
@ -2645,7 +2645,7 @@ impl Render for InlineAssistant {
|
||||||
.justify_center()
|
.justify_center()
|
||||||
.w(measurements.gutter_width)
|
.w(measurements.gutter_width)
|
||||||
.child(
|
.child(
|
||||||
IconButton::new("include_conversation", Icon::Ai)
|
IconButton::new("include_conversation", IconName::Ai)
|
||||||
.on_click(cx.listener(|this, _, cx| {
|
.on_click(cx.listener(|this, _, cx| {
|
||||||
this.toggle_include_conversation(&ToggleIncludeConversation, cx)
|
this.toggle_include_conversation(&ToggleIncludeConversation, cx)
|
||||||
}))
|
}))
|
||||||
|
@ -2660,7 +2660,7 @@ impl Render for InlineAssistant {
|
||||||
)
|
)
|
||||||
.children(if SemanticIndex::enabled(cx) {
|
.children(if SemanticIndex::enabled(cx) {
|
||||||
Some(
|
Some(
|
||||||
IconButton::new("retrieve_context", Icon::MagnifyingGlass)
|
IconButton::new("retrieve_context", IconName::MagnifyingGlass)
|
||||||
.on_click(cx.listener(|this, _, cx| {
|
.on_click(cx.listener(|this, _, cx| {
|
||||||
this.toggle_retrieve_context(&ToggleRetrieveContext, cx)
|
this.toggle_retrieve_context(&ToggleRetrieveContext, cx)
|
||||||
}))
|
}))
|
||||||
|
@ -2682,7 +2682,7 @@ impl Render for InlineAssistant {
|
||||||
div()
|
div()
|
||||||
.id("error")
|
.id("error")
|
||||||
.tooltip(move |cx| Tooltip::text(error_message.clone(), cx))
|
.tooltip(move |cx| Tooltip::text(error_message.clone(), cx))
|
||||||
.child(IconElement::new(Icon::XCircle).color(Color::Error)),
|
.child(Icon::new(IconName::XCircle).color(Color::Error)),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
@ -2957,7 +2957,7 @@ impl InlineAssistant {
|
||||||
div()
|
div()
|
||||||
.id("error")
|
.id("error")
|
||||||
.tooltip(|cx| Tooltip::text("Not Authenticated. Please ensure you have a valid 'OPENAI_API_KEY' in your environment variables.", cx))
|
.tooltip(|cx| Tooltip::text("Not Authenticated. Please ensure you have a valid 'OPENAI_API_KEY' in your environment variables.", cx))
|
||||||
.child(IconElement::new(Icon::XCircle))
|
.child(Icon::new(IconName::XCircle))
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
),
|
),
|
||||||
|
|
||||||
|
@ -2965,7 +2965,7 @@ impl InlineAssistant {
|
||||||
div()
|
div()
|
||||||
.id("error")
|
.id("error")
|
||||||
.tooltip(|cx| Tooltip::text("Not Indexed", cx))
|
.tooltip(|cx| Tooltip::text("Not Indexed", cx))
|
||||||
.child(IconElement::new(Icon::XCircle))
|
.child(Icon::new(IconName::XCircle))
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
),
|
),
|
||||||
|
|
||||||
|
@ -2996,7 +2996,7 @@ impl InlineAssistant {
|
||||||
div()
|
div()
|
||||||
.id("update")
|
.id("update")
|
||||||
.tooltip(move |cx| Tooltip::text(status_text.clone(), cx))
|
.tooltip(move |cx| Tooltip::text(status_text.clone(), cx))
|
||||||
.child(IconElement::new(Icon::Update).color(Color::Info))
|
.child(Icon::new(IconName::Update).color(Color::Info))
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -3005,7 +3005,7 @@ impl InlineAssistant {
|
||||||
div()
|
div()
|
||||||
.id("check")
|
.id("check")
|
||||||
.tooltip(|cx| Tooltip::text("Index up to date", cx))
|
.tooltip(|cx| Tooltip::text("Index up to date", cx))
|
||||||
.child(IconElement::new(Icon::Check).color(Color::Success))
|
.child(Icon::new(IconName::Check).color(Color::Success))
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ use gpui::{
|
||||||
};
|
};
|
||||||
use menu::Cancel;
|
use menu::Cancel;
|
||||||
use util::channel::ReleaseChannel;
|
use util::channel::ReleaseChannel;
|
||||||
use workspace::ui::{h_stack, v_stack, Icon, IconElement, Label, StyledExt};
|
use workspace::ui::{h_stack, v_stack, Icon, IconName, Label, StyledExt};
|
||||||
|
|
||||||
pub struct UpdateNotification {
|
pub struct UpdateNotification {
|
||||||
version: SemanticVersion,
|
version: SemanticVersion,
|
||||||
|
@ -30,7 +30,7 @@ impl Render for UpdateNotification {
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.id("cancel")
|
.id("cancel")
|
||||||
.child(IconElement::new(Icon::Close))
|
.child(Icon::new(IconName::Close))
|
||||||
.cursor_pointer()
|
.cursor_pointer()
|
||||||
.on_click(cx.listener(|this, _, cx| this.dismiss(&menu::Cancel, cx))),
|
.on_click(cx.listener(|this, _, cx| this.dismiss(&menu::Cancel, cx))),
|
||||||
),
|
),
|
||||||
|
|
|
@ -9,9 +9,12 @@ pub struct CallSettings {
|
||||||
pub mute_on_join: bool,
|
pub mute_on_join: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Configuration of voice calls in Zed.
|
||||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
|
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
|
||||||
pub struct CallSettingsContent {
|
pub struct CallSettingsContent {
|
||||||
/// Whether the microphone should be muted when joining a channel or a call.
|
/// Whether the microphone should be muted when joining a channel or a call.
|
||||||
|
///
|
||||||
|
/// Default: false
|
||||||
pub mute_on_join: Option<bool>,
|
pub mute_on_join: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -173,7 +173,11 @@ impl Room {
|
||||||
cx.spawn(|this, mut cx| async move {
|
cx.spawn(|this, mut cx| async move {
|
||||||
connect.await?;
|
connect.await?;
|
||||||
|
|
||||||
if !cx.update(|cx| Self::mute_on_join(cx))? {
|
let is_read_only = this
|
||||||
|
.update(&mut cx, |room, _| room.read_only())
|
||||||
|
.unwrap_or(true);
|
||||||
|
|
||||||
|
if !cx.update(|cx| Self::mute_on_join(cx))? && !is_read_only {
|
||||||
this.update(&mut cx, |this, cx| this.share_microphone(cx))?
|
this.update(&mut cx, |this, cx| this.share_microphone(cx))?
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
@ -620,6 +624,27 @@ impl Room {
|
||||||
self.local_participant.role == proto::ChannelRole::Admin
|
self.local_participant.role == proto::ChannelRole::Admin
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_participant_role(
|
||||||
|
&mut self,
|
||||||
|
user_id: u64,
|
||||||
|
role: proto::ChannelRole,
|
||||||
|
cx: &ModelContext<Self>,
|
||||||
|
) -> Task<Result<()>> {
|
||||||
|
let client = self.client.clone();
|
||||||
|
let room_id = self.id;
|
||||||
|
let role = role.into();
|
||||||
|
cx.spawn(|_, _| async move {
|
||||||
|
client
|
||||||
|
.request(proto::SetRoomParticipantRole {
|
||||||
|
room_id,
|
||||||
|
user_id,
|
||||||
|
role,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map(|_| ())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn pending_participants(&self) -> &[Arc<User>] {
|
pub fn pending_participants(&self) -> &[Arc<User>] {
|
||||||
&self.pending_participants
|
&self.pending_participants
|
||||||
}
|
}
|
||||||
|
@ -729,9 +754,21 @@ impl Room {
|
||||||
if this.local_participant.role != role {
|
if this.local_participant.role != role {
|
||||||
this.local_participant.role = role;
|
this.local_participant.role = role;
|
||||||
|
|
||||||
|
if role == proto::ChannelRole::Guest {
|
||||||
|
for project in mem::take(&mut this.shared_projects) {
|
||||||
|
if let Some(project) = project.upgrade() {
|
||||||
|
this.unshare_project(project, cx).log_err();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.local_participant.projects.clear();
|
||||||
|
if let Some(live_kit_room) = &mut this.live_kit {
|
||||||
|
live_kit_room.stop_publishing(cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.joined_projects.retain(|project| {
|
this.joined_projects.retain(|project| {
|
||||||
if let Some(project) = project.upgrade() {
|
if let Some(project) = project.upgrade() {
|
||||||
project.update(cx, |project, _| project.set_role(role));
|
project.update(cx, |project, cx| project.set_role(role, cx));
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
|
@ -1607,6 +1644,24 @@ impl LiveKitRoom {
|
||||||
|
|
||||||
Ok((result, old_muted))
|
Ok((result, old_muted))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn stop_publishing(&mut self, cx: &mut ModelContext<Room>) {
|
||||||
|
if let LocalTrack::Published {
|
||||||
|
track_publication, ..
|
||||||
|
} = mem::replace(&mut self.microphone_track, LocalTrack::None)
|
||||||
|
{
|
||||||
|
self.room.unpublish_track(track_publication);
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
if let LocalTrack::Published {
|
||||||
|
track_publication, ..
|
||||||
|
} = mem::replace(&mut self.screen_track, LocalTrack::None)
|
||||||
|
{
|
||||||
|
self.room.unpublish_track(track_publication);
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum LocalTrack {
|
enum LocalTrack {
|
||||||
|
|
|
@ -3,7 +3,7 @@ use anyhow::{anyhow, Context, Result};
|
||||||
use collections::{hash_map::Entry, HashMap, HashSet};
|
use collections::{hash_map::Entry, HashMap, HashSet};
|
||||||
use feature_flags::FeatureFlagAppExt;
|
use feature_flags::FeatureFlagAppExt;
|
||||||
use futures::{channel::mpsc, Future, StreamExt};
|
use futures::{channel::mpsc, Future, StreamExt};
|
||||||
use gpui::{AsyncAppContext, EventEmitter, Model, ModelContext, SharedString, Task};
|
use gpui::{AsyncAppContext, EventEmitter, Model, ModelContext, SharedUrl, Task};
|
||||||
use postage::{sink::Sink, watch};
|
use postage::{sink::Sink, watch};
|
||||||
use rpc::proto::{RequestMessage, UsersResponse};
|
use rpc::proto::{RequestMessage, UsersResponse};
|
||||||
use std::sync::{Arc, Weak};
|
use std::sync::{Arc, Weak};
|
||||||
|
@ -19,7 +19,7 @@ pub struct ParticipantIndex(pub u32);
|
||||||
pub struct User {
|
pub struct User {
|
||||||
pub id: UserId,
|
pub id: UserId,
|
||||||
pub github_login: String,
|
pub github_login: String,
|
||||||
pub avatar_uri: SharedString,
|
pub avatar_uri: SharedUrl,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
|
|
@ -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.34.0"
|
version = "0.36.0"
|
||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
|
@ -74,6 +74,8 @@ live_kit_client = { path = "../live_kit_client", features = ["test-support"] }
|
||||||
lsp = { path = "../lsp", features = ["test-support"] }
|
lsp = { path = "../lsp", features = ["test-support"] }
|
||||||
node_runtime = { path = "../node_runtime" }
|
node_runtime = { path = "../node_runtime" }
|
||||||
notifications = { path = "../notifications", features = ["test-support"] }
|
notifications = { path = "../notifications", features = ["test-support"] }
|
||||||
|
file_finder = { path = "../file_finder"}
|
||||||
|
menu = { path = "../menu"}
|
||||||
|
|
||||||
project = { path = "../project", features = ["test-support"] }
|
project = { path = "../project", features = ["test-support"] }
|
||||||
rpc = { path = "../rpc", features = ["test-support"] }
|
rpc = { path = "../rpc", features = ["test-support"] }
|
||||||
|
|
|
@ -133,13 +133,29 @@ impl ChannelRole {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn can_share_projects(&self) -> bool {
|
pub fn can_publish_to_rooms(&self) -> bool {
|
||||||
use ChannelRole::*;
|
use ChannelRole::*;
|
||||||
match self {
|
match self {
|
||||||
Admin | Member => true,
|
Admin | Member => true,
|
||||||
Guest | Banned => false,
|
Guest | Banned => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn can_edit_projects(&self) -> bool {
|
||||||
|
use ChannelRole::*;
|
||||||
|
match self {
|
||||||
|
Admin | Member => true,
|
||||||
|
Guest | Banned => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn can_read_projects(&self) -> bool {
|
||||||
|
use ChannelRole::*;
|
||||||
|
match self {
|
||||||
|
Admin | Member | Guest => true,
|
||||||
|
Banned => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<proto::ChannelRole> for ChannelRole {
|
impl From<proto::ChannelRole> for ChannelRole {
|
||||||
|
|
|
@ -49,7 +49,7 @@ impl Database {
|
||||||
if !participant
|
if !participant
|
||||||
.role
|
.role
|
||||||
.unwrap_or(ChannelRole::Member)
|
.unwrap_or(ChannelRole::Member)
|
||||||
.can_share_projects()
|
.can_publish_to_rooms()
|
||||||
{
|
{
|
||||||
return Err(anyhow!("guests cannot share projects"))?;
|
return Err(anyhow!("guests cannot share projects"))?;
|
||||||
}
|
}
|
||||||
|
@ -777,13 +777,131 @@ impl Database {
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn project_collaborators(
|
pub async fn check_user_is_project_host(
|
||||||
&self,
|
&self,
|
||||||
project_id: ProjectId,
|
project_id: ProjectId,
|
||||||
connection_id: ConnectionId,
|
connection_id: ConnectionId,
|
||||||
|
) -> Result<()> {
|
||||||
|
let room_id = self.room_id_for_project(project_id).await?;
|
||||||
|
self.room_transaction(room_id, |tx| async move {
|
||||||
|
project_collaborator::Entity::find()
|
||||||
|
.filter(
|
||||||
|
Condition::all()
|
||||||
|
.add(project_collaborator::Column::ProjectId.eq(project_id))
|
||||||
|
.add(project_collaborator::Column::IsHost.eq(true))
|
||||||
|
.add(project_collaborator::Column::ConnectionId.eq(connection_id.id))
|
||||||
|
.add(
|
||||||
|
project_collaborator::Column::ConnectionServerId
|
||||||
|
.eq(connection_id.owner_id),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.one(&*tx)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| anyhow!("failed to read project host"))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map(|guard| guard.into_inner())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn host_for_read_only_project_request(
|
||||||
|
&self,
|
||||||
|
project_id: ProjectId,
|
||||||
|
connection_id: ConnectionId,
|
||||||
|
) -> Result<ConnectionId> {
|
||||||
|
let room_id = self.room_id_for_project(project_id).await?;
|
||||||
|
self.room_transaction(room_id, |tx| async move {
|
||||||
|
let current_participant = room_participant::Entity::find()
|
||||||
|
.filter(room_participant::Column::RoomId.eq(room_id))
|
||||||
|
.filter(room_participant::Column::AnsweringConnectionId.eq(connection_id.id))
|
||||||
|
.one(&*tx)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| anyhow!("no such room"))?;
|
||||||
|
|
||||||
|
if !current_participant
|
||||||
|
.role
|
||||||
|
.map_or(false, |role| role.can_read_projects())
|
||||||
|
{
|
||||||
|
Err(anyhow!("not authorized to read projects"))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let host = project_collaborator::Entity::find()
|
||||||
|
.filter(
|
||||||
|
project_collaborator::Column::ProjectId
|
||||||
|
.eq(project_id)
|
||||||
|
.and(project_collaborator::Column::IsHost.eq(true)),
|
||||||
|
)
|
||||||
|
.one(&*tx)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| anyhow!("failed to read project host"))?;
|
||||||
|
|
||||||
|
Ok(host.connection())
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map(|guard| guard.into_inner())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn host_for_mutating_project_request(
|
||||||
|
&self,
|
||||||
|
project_id: ProjectId,
|
||||||
|
connection_id: ConnectionId,
|
||||||
|
) -> Result<ConnectionId> {
|
||||||
|
let room_id = self.room_id_for_project(project_id).await?;
|
||||||
|
self.room_transaction(room_id, |tx| async move {
|
||||||
|
let current_participant = room_participant::Entity::find()
|
||||||
|
.filter(room_participant::Column::RoomId.eq(room_id))
|
||||||
|
.filter(room_participant::Column::AnsweringConnectionId.eq(connection_id.id))
|
||||||
|
.one(&*tx)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| anyhow!("no such room"))?;
|
||||||
|
|
||||||
|
if !current_participant
|
||||||
|
.role
|
||||||
|
.map_or(false, |role| role.can_edit_projects())
|
||||||
|
{
|
||||||
|
Err(anyhow!("not authorized to edit projects"))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let host = project_collaborator::Entity::find()
|
||||||
|
.filter(
|
||||||
|
project_collaborator::Column::ProjectId
|
||||||
|
.eq(project_id)
|
||||||
|
.and(project_collaborator::Column::IsHost.eq(true)),
|
||||||
|
)
|
||||||
|
.one(&*tx)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| anyhow!("failed to read project host"))?;
|
||||||
|
|
||||||
|
Ok(host.connection())
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map(|guard| guard.into_inner())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn project_collaborators_for_buffer_update(
|
||||||
|
&self,
|
||||||
|
project_id: ProjectId,
|
||||||
|
connection_id: ConnectionId,
|
||||||
|
requires_write: bool,
|
||||||
) -> Result<RoomGuard<Vec<ProjectCollaborator>>> {
|
) -> Result<RoomGuard<Vec<ProjectCollaborator>>> {
|
||||||
let room_id = self.room_id_for_project(project_id).await?;
|
let room_id = self.room_id_for_project(project_id).await?;
|
||||||
self.room_transaction(room_id, |tx| async move {
|
self.room_transaction(room_id, |tx| async move {
|
||||||
|
let current_participant = room_participant::Entity::find()
|
||||||
|
.filter(room_participant::Column::RoomId.eq(room_id))
|
||||||
|
.filter(room_participant::Column::AnsweringConnectionId.eq(connection_id.id))
|
||||||
|
.one(&*tx)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| anyhow!("no such room"))?;
|
||||||
|
|
||||||
|
if requires_write
|
||||||
|
&& !current_participant
|
||||||
|
.role
|
||||||
|
.map_or(false, |role| role.can_edit_projects())
|
||||||
|
{
|
||||||
|
Err(anyhow!("not authorized to edit projects"))?;
|
||||||
|
}
|
||||||
|
|
||||||
let collaborators = project_collaborator::Entity::find()
|
let collaborators = project_collaborator::Entity::find()
|
||||||
.filter(project_collaborator::Column::ProjectId.eq(project_id))
|
.filter(project_collaborator::Column::ProjectId.eq(project_id))
|
||||||
.all(&*tx)
|
.all(&*tx)
|
||||||
|
|
|
@ -1004,6 +1004,46 @@ impl Database {
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn set_room_participant_role(
|
||||||
|
&self,
|
||||||
|
admin_id: UserId,
|
||||||
|
room_id: RoomId,
|
||||||
|
user_id: UserId,
|
||||||
|
role: ChannelRole,
|
||||||
|
) -> Result<RoomGuard<proto::Room>> {
|
||||||
|
self.room_transaction(room_id, |tx| async move {
|
||||||
|
room_participant::Entity::find()
|
||||||
|
.filter(
|
||||||
|
Condition::all()
|
||||||
|
.add(room_participant::Column::RoomId.eq(room_id))
|
||||||
|
.add(room_participant::Column::UserId.eq(admin_id))
|
||||||
|
.add(room_participant::Column::Role.eq(ChannelRole::Admin)),
|
||||||
|
)
|
||||||
|
.one(&*tx)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| anyhow!("only admins can set participant role"))?;
|
||||||
|
|
||||||
|
let result = room_participant::Entity::update_many()
|
||||||
|
.filter(
|
||||||
|
Condition::all()
|
||||||
|
.add(room_participant::Column::RoomId.eq(room_id))
|
||||||
|
.add(room_participant::Column::UserId.eq(user_id)),
|
||||||
|
)
|
||||||
|
.set(room_participant::ActiveModel {
|
||||||
|
role: ActiveValue::set(Some(ChannelRole::from(role))),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.exec(&*tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if result.rows_affected != 1 {
|
||||||
|
Err(anyhow!("could not update room participant role"))?;
|
||||||
|
}
|
||||||
|
Ok(self.get_room(room_id, &tx).await?)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn connection_lost(&self, connection: ConnectionId) -> Result<()> {
|
pub async fn connection_lost(&self, connection: ConnectionId) -> Result<()> {
|
||||||
self.transaction(|tx| async move {
|
self.transaction(|tx| async move {
|
||||||
self.room_connection_lost(connection, &*tx).await?;
|
self.room_connection_lost(connection, &*tx).await?;
|
||||||
|
|
|
@ -103,6 +103,12 @@ pub struct Config {
|
||||||
pub zed_environment: Arc<str>,
|
pub zed_environment: Arc<str>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
pub fn is_development(&self) -> bool {
|
||||||
|
self.zed_environment == "development".into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Default, Deserialize)]
|
#[derive(Default, Deserialize)]
|
||||||
pub struct MigrateConfig {
|
pub struct MigrateConfig {
|
||||||
pub database_url: String,
|
pub database_url: String,
|
||||||
|
|
|
@ -53,6 +53,25 @@ async fn main() -> Result<()> {
|
||||||
let config = envy::from_env::<Config>().expect("error loading config");
|
let config = envy::from_env::<Config>().expect("error loading config");
|
||||||
init_tracing(&config);
|
init_tracing(&config);
|
||||||
|
|
||||||
|
if config.is_development() {
|
||||||
|
// sanity check database url so even if we deploy a busted ZED_ENVIRONMENT to production
|
||||||
|
// we do not run
|
||||||
|
if config.database_url != "postgres://postgres@localhost/zed" {
|
||||||
|
panic!("about to run development migrations on a non-development database?")
|
||||||
|
}
|
||||||
|
let migrations_path = Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/migrations"));
|
||||||
|
let db_options = db::ConnectOptions::new(config.database_url.clone());
|
||||||
|
let db = Database::new(db_options, Executor::Production).await?;
|
||||||
|
|
||||||
|
let migrations = db.migrate(&migrations_path, false).await?;
|
||||||
|
for (migration, duration) in migrations {
|
||||||
|
println!(
|
||||||
|
"Ran {} {} {:?}",
|
||||||
|
migration.version, migration.description, duration
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let state = AppState::new(config).await?;
|
let state = AppState::new(config).await?;
|
||||||
|
|
||||||
let listener = TcpListener::bind(&format!("0.0.0.0:{}", state.config.http_port))
|
let listener = TcpListener::bind(&format!("0.0.0.0:{}", state.config.http_port))
|
||||||
|
|
|
@ -42,7 +42,7 @@ use prometheus::{register_int_gauge, IntGauge};
|
||||||
use rpc::{
|
use rpc::{
|
||||||
proto::{
|
proto::{
|
||||||
self, Ack, AnyTypedEnvelope, EntityMessage, EnvelopedMessage, LiveKitConnectionInfo,
|
self, Ack, AnyTypedEnvelope, EntityMessage, EnvelopedMessage, LiveKitConnectionInfo,
|
||||||
RequestMessage, UpdateChannelBufferCollaborators,
|
RequestMessage, ShareProject, UpdateChannelBufferCollaborators,
|
||||||
},
|
},
|
||||||
Connection, ConnectionId, Peer, Receipt, TypedEnvelope,
|
Connection, ConnectionId, Peer, Receipt, TypedEnvelope,
|
||||||
};
|
};
|
||||||
|
@ -202,6 +202,7 @@ impl Server {
|
||||||
.add_request_handler(join_room)
|
.add_request_handler(join_room)
|
||||||
.add_request_handler(rejoin_room)
|
.add_request_handler(rejoin_room)
|
||||||
.add_request_handler(leave_room)
|
.add_request_handler(leave_room)
|
||||||
|
.add_request_handler(set_room_participant_role)
|
||||||
.add_request_handler(call)
|
.add_request_handler(call)
|
||||||
.add_request_handler(cancel_call)
|
.add_request_handler(cancel_call)
|
||||||
.add_message_handler(decline_call)
|
.add_message_handler(decline_call)
|
||||||
|
@ -216,40 +217,45 @@ impl Server {
|
||||||
.add_message_handler(update_language_server)
|
.add_message_handler(update_language_server)
|
||||||
.add_message_handler(update_diagnostic_summary)
|
.add_message_handler(update_diagnostic_summary)
|
||||||
.add_message_handler(update_worktree_settings)
|
.add_message_handler(update_worktree_settings)
|
||||||
.add_message_handler(refresh_inlay_hints)
|
.add_request_handler(forward_read_only_project_request::<proto::GetHover>)
|
||||||
.add_request_handler(forward_project_request::<proto::GetHover>)
|
.add_request_handler(forward_read_only_project_request::<proto::GetDefinition>)
|
||||||
.add_request_handler(forward_project_request::<proto::GetDefinition>)
|
.add_request_handler(forward_read_only_project_request::<proto::GetTypeDefinition>)
|
||||||
.add_request_handler(forward_project_request::<proto::GetTypeDefinition>)
|
.add_request_handler(forward_read_only_project_request::<proto::GetReferences>)
|
||||||
.add_request_handler(forward_project_request::<proto::GetReferences>)
|
.add_request_handler(forward_read_only_project_request::<proto::SearchProject>)
|
||||||
.add_request_handler(forward_project_request::<proto::SearchProject>)
|
.add_request_handler(forward_read_only_project_request::<proto::GetDocumentHighlights>)
|
||||||
.add_request_handler(forward_project_request::<proto::GetDocumentHighlights>)
|
.add_request_handler(forward_read_only_project_request::<proto::GetProjectSymbols>)
|
||||||
.add_request_handler(forward_project_request::<proto::GetProjectSymbols>)
|
.add_request_handler(forward_read_only_project_request::<proto::OpenBufferForSymbol>)
|
||||||
.add_request_handler(forward_project_request::<proto::OpenBufferForSymbol>)
|
.add_request_handler(forward_read_only_project_request::<proto::OpenBufferById>)
|
||||||
.add_request_handler(forward_project_request::<proto::OpenBufferById>)
|
.add_request_handler(forward_read_only_project_request::<proto::SynchronizeBuffers>)
|
||||||
.add_request_handler(forward_project_request::<proto::OpenBufferByPath>)
|
.add_request_handler(forward_read_only_project_request::<proto::InlayHints>)
|
||||||
.add_request_handler(forward_project_request::<proto::GetCompletions>)
|
.add_request_handler(forward_read_only_project_request::<proto::OpenBufferByPath>)
|
||||||
.add_request_handler(forward_project_request::<proto::ApplyCompletionAdditionalEdits>)
|
.add_request_handler(forward_mutating_project_request::<proto::GetCompletions>)
|
||||||
.add_request_handler(forward_project_request::<proto::ResolveCompletionDocumentation>)
|
.add_request_handler(
|
||||||
.add_request_handler(forward_project_request::<proto::GetCodeActions>)
|
forward_mutating_project_request::<proto::ApplyCompletionAdditionalEdits>,
|
||||||
.add_request_handler(forward_project_request::<proto::ApplyCodeAction>)
|
)
|
||||||
.add_request_handler(forward_project_request::<proto::PrepareRename>)
|
.add_request_handler(
|
||||||
.add_request_handler(forward_project_request::<proto::PerformRename>)
|
forward_mutating_project_request::<proto::ResolveCompletionDocumentation>,
|
||||||
.add_request_handler(forward_project_request::<proto::ReloadBuffers>)
|
)
|
||||||
.add_request_handler(forward_project_request::<proto::SynchronizeBuffers>)
|
.add_request_handler(forward_mutating_project_request::<proto::GetCodeActions>)
|
||||||
.add_request_handler(forward_project_request::<proto::FormatBuffers>)
|
.add_request_handler(forward_mutating_project_request::<proto::ApplyCodeAction>)
|
||||||
.add_request_handler(forward_project_request::<proto::CreateProjectEntry>)
|
.add_request_handler(forward_mutating_project_request::<proto::PrepareRename>)
|
||||||
.add_request_handler(forward_project_request::<proto::RenameProjectEntry>)
|
.add_request_handler(forward_mutating_project_request::<proto::PerformRename>)
|
||||||
.add_request_handler(forward_project_request::<proto::CopyProjectEntry>)
|
.add_request_handler(forward_mutating_project_request::<proto::ReloadBuffers>)
|
||||||
.add_request_handler(forward_project_request::<proto::DeleteProjectEntry>)
|
.add_request_handler(forward_mutating_project_request::<proto::FormatBuffers>)
|
||||||
.add_request_handler(forward_project_request::<proto::ExpandProjectEntry>)
|
.add_request_handler(forward_mutating_project_request::<proto::CreateProjectEntry>)
|
||||||
.add_request_handler(forward_project_request::<proto::OnTypeFormatting>)
|
.add_request_handler(forward_mutating_project_request::<proto::RenameProjectEntry>)
|
||||||
.add_request_handler(forward_project_request::<proto::InlayHints>)
|
.add_request_handler(forward_mutating_project_request::<proto::CopyProjectEntry>)
|
||||||
|
.add_request_handler(forward_mutating_project_request::<proto::DeleteProjectEntry>)
|
||||||
|
.add_request_handler(forward_mutating_project_request::<proto::ExpandProjectEntry>)
|
||||||
|
.add_request_handler(forward_mutating_project_request::<proto::OnTypeFormatting>)
|
||||||
|
.add_request_handler(forward_mutating_project_request::<proto::SaveBuffer>)
|
||||||
.add_message_handler(create_buffer_for_peer)
|
.add_message_handler(create_buffer_for_peer)
|
||||||
.add_request_handler(update_buffer)
|
.add_request_handler(update_buffer)
|
||||||
.add_message_handler(update_buffer_file)
|
.add_message_handler(broadcast_project_message_from_host::<proto::RefreshInlayHints>)
|
||||||
.add_message_handler(buffer_reloaded)
|
.add_message_handler(broadcast_project_message_from_host::<proto::UpdateBufferFile>)
|
||||||
.add_message_handler(buffer_saved)
|
.add_message_handler(broadcast_project_message_from_host::<proto::BufferReloaded>)
|
||||||
.add_request_handler(forward_project_request::<proto::SaveBuffer>)
|
.add_message_handler(broadcast_project_message_from_host::<proto::BufferSaved>)
|
||||||
|
.add_message_handler(broadcast_project_message_from_host::<proto::UpdateDiffBase>)
|
||||||
.add_request_handler(get_users)
|
.add_request_handler(get_users)
|
||||||
.add_request_handler(fuzzy_search_users)
|
.add_request_handler(fuzzy_search_users)
|
||||||
.add_request_handler(request_contact)
|
.add_request_handler(request_contact)
|
||||||
|
@ -281,7 +287,6 @@ impl Server {
|
||||||
.add_request_handler(follow)
|
.add_request_handler(follow)
|
||||||
.add_message_handler(unfollow)
|
.add_message_handler(unfollow)
|
||||||
.add_message_handler(update_followers)
|
.add_message_handler(update_followers)
|
||||||
.add_message_handler(update_diff_base)
|
|
||||||
.add_request_handler(get_private_user_info)
|
.add_request_handler(get_private_user_info)
|
||||||
.add_message_handler(acknowledge_channel_message)
|
.add_message_handler(acknowledge_channel_message)
|
||||||
.add_message_handler(acknowledge_buffer_version);
|
.add_message_handler(acknowledge_buffer_version);
|
||||||
|
@ -1254,6 +1259,50 @@ async fn leave_room(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn set_room_participant_role(
|
||||||
|
request: proto::SetRoomParticipantRole,
|
||||||
|
response: Response<proto::SetRoomParticipantRole>,
|
||||||
|
session: Session,
|
||||||
|
) -> Result<()> {
|
||||||
|
let (live_kit_room, can_publish) = {
|
||||||
|
let room = session
|
||||||
|
.db()
|
||||||
|
.await
|
||||||
|
.set_room_participant_role(
|
||||||
|
session.user_id,
|
||||||
|
RoomId::from_proto(request.room_id),
|
||||||
|
UserId::from_proto(request.user_id),
|
||||||
|
ChannelRole::from(request.role()),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let live_kit_room = room.live_kit_room.clone();
|
||||||
|
let can_publish = ChannelRole::from(request.role()).can_publish_to_rooms();
|
||||||
|
room_updated(&room, &session.peer);
|
||||||
|
(live_kit_room, can_publish)
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(live_kit) = session.live_kit_client.as_ref() {
|
||||||
|
live_kit
|
||||||
|
.update_participant(
|
||||||
|
live_kit_room.clone(),
|
||||||
|
request.user_id.to_string(),
|
||||||
|
live_kit_server::proto::ParticipantPermission {
|
||||||
|
can_subscribe: true,
|
||||||
|
can_publish,
|
||||||
|
can_publish_data: can_publish,
|
||||||
|
hidden: false,
|
||||||
|
recorder: false,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.trace_err();
|
||||||
|
}
|
||||||
|
|
||||||
|
response.send(proto::Ack {})?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
async fn call(
|
async fn call(
|
||||||
request: proto::Call,
|
request: proto::Call,
|
||||||
response: Response<proto::Call>,
|
response: Response<proto::Call>,
|
||||||
|
@ -1694,10 +1743,6 @@ async fn update_worktree_settings(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn refresh_inlay_hints(request: proto::RefreshInlayHints, session: Session) -> Result<()> {
|
|
||||||
broadcast_project_message(request.project_id, request, session).await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn start_language_server(
|
async fn start_language_server(
|
||||||
request: proto::StartLanguageServer,
|
request: proto::StartLanguageServer,
|
||||||
session: Session,
|
session: Session,
|
||||||
|
@ -1742,7 +1787,7 @@ async fn update_language_server(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn forward_project_request<T>(
|
async fn forward_read_only_project_request<T>(
|
||||||
request: T,
|
request: T,
|
||||||
response: Response<T>,
|
response: Response<T>,
|
||||||
session: Session,
|
session: Session,
|
||||||
|
@ -1751,24 +1796,37 @@ where
|
||||||
T: EntityMessage + RequestMessage,
|
T: EntityMessage + RequestMessage,
|
||||||
{
|
{
|
||||||
let project_id = ProjectId::from_proto(request.remote_entity_id());
|
let project_id = ProjectId::from_proto(request.remote_entity_id());
|
||||||
let host_connection_id = {
|
let host_connection_id = session
|
||||||
let collaborators = session
|
.db()
|
||||||
.db()
|
.await
|
||||||
.await
|
.host_for_read_only_project_request(project_id, session.connection_id)
|
||||||
.project_collaborators(project_id, session.connection_id)
|
.await?;
|
||||||
.await?;
|
|
||||||
collaborators
|
|
||||||
.iter()
|
|
||||||
.find(|collaborator| collaborator.is_host)
|
|
||||||
.ok_or_else(|| anyhow!("host not found"))?
|
|
||||||
.connection_id
|
|
||||||
};
|
|
||||||
|
|
||||||
let payload = session
|
let payload = session
|
||||||
.peer
|
.peer
|
||||||
.forward_request(session.connection_id, host_connection_id, request)
|
.forward_request(session.connection_id, host_connection_id, request)
|
||||||
.await?;
|
.await?;
|
||||||
|
response.send(payload)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn forward_mutating_project_request<T>(
|
||||||
|
request: T,
|
||||||
|
response: Response<T>,
|
||||||
|
session: Session,
|
||||||
|
) -> Result<()>
|
||||||
|
where
|
||||||
|
T: EntityMessage + RequestMessage,
|
||||||
|
{
|
||||||
|
let project_id = ProjectId::from_proto(request.remote_entity_id());
|
||||||
|
let host_connection_id = session
|
||||||
|
.db()
|
||||||
|
.await
|
||||||
|
.host_for_mutating_project_request(project_id, session.connection_id)
|
||||||
|
.await?;
|
||||||
|
let payload = session
|
||||||
|
.peer
|
||||||
|
.forward_request(session.connection_id, host_connection_id, request)
|
||||||
|
.await?;
|
||||||
response.send(payload)?;
|
response.send(payload)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -1777,6 +1835,14 @@ async fn create_buffer_for_peer(
|
||||||
request: proto::CreateBufferForPeer,
|
request: proto::CreateBufferForPeer,
|
||||||
session: Session,
|
session: Session,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
|
session
|
||||||
|
.db()
|
||||||
|
.await
|
||||||
|
.check_user_is_project_host(
|
||||||
|
ProjectId::from_proto(request.project_id),
|
||||||
|
session.connection_id,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
let peer_id = request.peer_id.ok_or_else(|| anyhow!("invalid peer id"))?;
|
let peer_id = request.peer_id.ok_or_else(|| anyhow!("invalid peer id"))?;
|
||||||
session
|
session
|
||||||
.peer
|
.peer
|
||||||
|
@ -1792,11 +1858,25 @@ async fn update_buffer(
|
||||||
let project_id = ProjectId::from_proto(request.project_id);
|
let project_id = ProjectId::from_proto(request.project_id);
|
||||||
let mut guest_connection_ids;
|
let mut guest_connection_ids;
|
||||||
let mut host_connection_id = None;
|
let mut host_connection_id = None;
|
||||||
|
|
||||||
|
let mut requires_write_permission = false;
|
||||||
|
|
||||||
|
for op in request.operations.iter() {
|
||||||
|
match op.variant {
|
||||||
|
None | Some(proto::operation::Variant::UpdateSelections(_)) => {}
|
||||||
|
Some(_) => requires_write_permission = true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
let collaborators = session
|
let collaborators = session
|
||||||
.db()
|
.db()
|
||||||
.await
|
.await
|
||||||
.project_collaborators(project_id, session.connection_id)
|
.project_collaborators_for_buffer_update(
|
||||||
|
project_id,
|
||||||
|
session.connection_id,
|
||||||
|
requires_write_permission,
|
||||||
|
)
|
||||||
.await?;
|
.await?;
|
||||||
guest_connection_ids = Vec::with_capacity(collaborators.len() - 1);
|
guest_connection_ids = Vec::with_capacity(collaborators.len() - 1);
|
||||||
for collaborator in collaborators.iter() {
|
for collaborator in collaborators.iter() {
|
||||||
|
@ -1829,60 +1909,17 @@ async fn update_buffer(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn update_buffer_file(request: proto::UpdateBufferFile, session: Session) -> Result<()> {
|
async fn broadcast_project_message_from_host<T: EntityMessage<Entity = ShareProject>>(
|
||||||
let project_id = ProjectId::from_proto(request.project_id);
|
|
||||||
let project_connection_ids = session
|
|
||||||
.db()
|
|
||||||
.await
|
|
||||||
.project_connection_ids(project_id, session.connection_id)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
broadcast(
|
|
||||||
Some(session.connection_id),
|
|
||||||
project_connection_ids.iter().copied(),
|
|
||||||
|connection_id| {
|
|
||||||
session
|
|
||||||
.peer
|
|
||||||
.forward_send(session.connection_id, connection_id, request.clone())
|
|
||||||
},
|
|
||||||
);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn buffer_reloaded(request: proto::BufferReloaded, session: Session) -> Result<()> {
|
|
||||||
let project_id = ProjectId::from_proto(request.project_id);
|
|
||||||
let project_connection_ids = session
|
|
||||||
.db()
|
|
||||||
.await
|
|
||||||
.project_connection_ids(project_id, session.connection_id)
|
|
||||||
.await?;
|
|
||||||
broadcast(
|
|
||||||
Some(session.connection_id),
|
|
||||||
project_connection_ids.iter().copied(),
|
|
||||||
|connection_id| {
|
|
||||||
session
|
|
||||||
.peer
|
|
||||||
.forward_send(session.connection_id, connection_id, request.clone())
|
|
||||||
},
|
|
||||||
);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn buffer_saved(request: proto::BufferSaved, session: Session) -> Result<()> {
|
|
||||||
broadcast_project_message(request.project_id, request, session).await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn broadcast_project_message<T: EnvelopedMessage>(
|
|
||||||
project_id: u64,
|
|
||||||
request: T,
|
request: T,
|
||||||
session: Session,
|
session: Session,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let project_id = ProjectId::from_proto(project_id);
|
let project_id = ProjectId::from_proto(request.remote_entity_id());
|
||||||
let project_connection_ids = session
|
let project_connection_ids = session
|
||||||
.db()
|
.db()
|
||||||
.await
|
.await
|
||||||
.project_connection_ids(project_id, session.connection_id)
|
.project_connection_ids(project_id, session.connection_id)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
broadcast(
|
broadcast(
|
||||||
Some(session.connection_id),
|
Some(session.connection_id),
|
||||||
project_connection_ids.iter().copied(),
|
project_connection_ids.iter().copied(),
|
||||||
|
@ -3111,25 +3148,6 @@ async fn mark_notification_as_read(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn update_diff_base(request: proto::UpdateDiffBase, session: Session) -> Result<()> {
|
|
||||||
let project_id = ProjectId::from_proto(request.project_id);
|
|
||||||
let project_connection_ids = session
|
|
||||||
.db()
|
|
||||||
.await
|
|
||||||
.project_connection_ids(project_id, session.connection_id)
|
|
||||||
.await?;
|
|
||||||
broadcast(
|
|
||||||
Some(session.connection_id),
|
|
||||||
project_connection_ids.iter().copied(),
|
|
||||||
|connection_id| {
|
|
||||||
session
|
|
||||||
.peer
|
|
||||||
.forward_send(session.connection_id, connection_id, request.clone())
|
|
||||||
},
|
|
||||||
);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_private_user_info(
|
async fn get_private_user_info(
|
||||||
_request: proto::GetPrivateUserInfo,
|
_request: proto::GetPrivateUserInfo,
|
||||||
response: Response<proto::GetPrivateUserInfo>,
|
response: Response<proto::GetPrivateUserInfo>,
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
use crate::tests::TestServer;
|
use crate::tests::TestServer;
|
||||||
use call::ActiveCall;
|
use call::ActiveCall;
|
||||||
use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
|
use editor::Editor;
|
||||||
|
use gpui::{BackgroundExecutor, TestAppContext};
|
||||||
use rpc::proto;
|
use rpc::proto;
|
||||||
use workspace::Workspace;
|
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_channel_guests(
|
async fn test_channel_guests(
|
||||||
|
@ -13,37 +13,18 @@ async fn test_channel_guests(
|
||||||
let mut server = TestServer::start(executor.clone()).await;
|
let mut server = TestServer::start(executor.clone()).await;
|
||||||
let client_a = server.create_client(cx_a, "user_a").await;
|
let client_a = server.create_client(cx_a, "user_a").await;
|
||||||
let client_b = server.create_client(cx_b, "user_b").await;
|
let client_b = server.create_client(cx_b, "user_b").await;
|
||||||
|
|
||||||
let channel_id = server
|
|
||||||
.make_channel("the-channel", None, (&client_a, cx_a), &mut [])
|
|
||||||
.await;
|
|
||||||
|
|
||||||
client_a
|
|
||||||
.channel_store()
|
|
||||||
.update(cx_a, |channel_store, cx| {
|
|
||||||
channel_store.set_channel_visibility(channel_id, proto::ChannelVisibility::Public, cx)
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
client_a
|
|
||||||
.fs()
|
|
||||||
.insert_tree(
|
|
||||||
"/a",
|
|
||||||
serde_json::json!({
|
|
||||||
"a.txt": "a-contents",
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let active_call_a = cx_a.read(ActiveCall::global);
|
let active_call_a = cx_a.read(ActiveCall::global);
|
||||||
|
|
||||||
|
let channel_id = server
|
||||||
|
.make_public_channel("the-channel", &client_a, cx_a)
|
||||||
|
.await;
|
||||||
|
|
||||||
// Client A shares a project in the channel
|
// Client A shares a project in the channel
|
||||||
|
let project_a = client_a.build_test_project(cx_a).await;
|
||||||
active_call_a
|
active_call_a
|
||||||
.update(cx_a, |call, cx| call.join_channel(channel_id, cx))
|
.update(cx_a, |call, cx| call.join_channel(channel_id, cx))
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
|
|
||||||
let project_id = active_call_a
|
let project_id = active_call_a
|
||||||
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
||||||
.await
|
.await
|
||||||
|
@ -57,30 +38,122 @@ async fn test_channel_guests(
|
||||||
|
|
||||||
// b should be following a in the shared project.
|
// b should be following a in the shared project.
|
||||||
// B is a guest,
|
// B is a guest,
|
||||||
cx_a.executor().run_until_parked();
|
executor.run_until_parked();
|
||||||
|
|
||||||
// todo!() the test window does not call activation handlers
|
let active_call_b = cx_b.read(ActiveCall::global);
|
||||||
// correctly yet, so this API does not work.
|
let project_b =
|
||||||
// let project_b = active_call_b.read_with(cx_b, |call, _| {
|
active_call_b.read_with(cx_b, |call, _| call.location().unwrap().upgrade().unwrap());
|
||||||
// call.location()
|
let room_b = active_call_b.update(cx_b, |call, _| call.room().unwrap().clone());
|
||||||
// .unwrap()
|
|
||||||
// .upgrade()
|
|
||||||
// .expect("should not be weak")
|
|
||||||
// });
|
|
||||||
|
|
||||||
let window_b = cx_b.update(|cx| cx.active_window().unwrap());
|
|
||||||
let cx_b = &mut VisualTestContext::from_window(window_b, cx_b);
|
|
||||||
|
|
||||||
let workspace_b = window_b
|
|
||||||
.downcast::<Workspace>()
|
|
||||||
.unwrap()
|
|
||||||
.root_view(cx_b)
|
|
||||||
.unwrap();
|
|
||||||
let project_b = workspace_b.update(cx_b, |workspace, _| workspace.project().clone());
|
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
project_b.read_with(cx_b, |project, _| project.remote_id()),
|
project_b.read_with(cx_b, |project, _| project.remote_id()),
|
||||||
Some(project_id),
|
Some(project_id),
|
||||||
);
|
);
|
||||||
assert!(project_b.read_with(cx_b, |project, _| project.is_read_only()))
|
assert!(project_b.read_with(cx_b, |project, _| project.is_read_only()));
|
||||||
|
assert!(project_b
|
||||||
|
.update(cx_b, |project, cx| {
|
||||||
|
let worktree_id = project.worktrees().next().unwrap().read(cx).id();
|
||||||
|
project.create_entry((worktree_id, "b.txt"), false, cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.is_err());
|
||||||
|
assert!(room_b.read_with(cx_b, |room, _| !room.is_sharing_mic()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_channel_guest_promotion(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
||||||
|
let mut server = TestServer::start(cx_a.executor()).await;
|
||||||
|
let client_a = server.create_client(cx_a, "user_a").await;
|
||||||
|
let client_b = server.create_client(cx_b, "user_b").await;
|
||||||
|
let active_call_a = cx_a.read(ActiveCall::global);
|
||||||
|
|
||||||
|
let channel_id = server
|
||||||
|
.make_public_channel("the-channel", &client_a, cx_a)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let project_a = client_a.build_test_project(cx_a).await;
|
||||||
|
cx_a.update(|cx| workspace::join_channel(channel_id, client_a.app_state.clone(), None, cx))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Client A shares a project in the channel
|
||||||
|
active_call_a
|
||||||
|
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
cx_a.run_until_parked();
|
||||||
|
|
||||||
|
// Client B joins channel A as a guest
|
||||||
|
cx_b.update(|cx| workspace::join_channel(channel_id, client_b.app_state.clone(), None, cx))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
cx_a.run_until_parked();
|
||||||
|
|
||||||
|
// client B opens 1.txt as a guest
|
||||||
|
let (workspace_b, cx_b) = client_b.active_workspace(cx_b);
|
||||||
|
let room_b = cx_b
|
||||||
|
.read(ActiveCall::global)
|
||||||
|
.update(cx_b, |call, _| call.room().unwrap().clone());
|
||||||
|
cx_b.simulate_keystrokes("cmd-p 1 enter");
|
||||||
|
|
||||||
|
let (project_b, editor_b) = workspace_b.update(cx_b, |workspace, cx| {
|
||||||
|
(
|
||||||
|
workspace.project().clone(),
|
||||||
|
workspace.active_item_as::<Editor>(cx).unwrap(),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
assert!(project_b.read_with(cx_b, |project, _| project.is_read_only()));
|
||||||
|
assert!(editor_b.update(cx_b, |e, cx| e.read_only(cx)));
|
||||||
|
assert!(dbg!(
|
||||||
|
room_b
|
||||||
|
.update(cx_b, |room, cx| room.share_microphone(cx))
|
||||||
|
.await
|
||||||
|
)
|
||||||
|
.is_err());
|
||||||
|
|
||||||
|
// B is promoted
|
||||||
|
active_call_a
|
||||||
|
.update(cx_a, |call, cx| {
|
||||||
|
call.room().unwrap().update(cx, |room, cx| {
|
||||||
|
room.set_participant_role(
|
||||||
|
client_b.user_id().unwrap(),
|
||||||
|
proto::ChannelRole::Member,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
cx_a.run_until_parked();
|
||||||
|
|
||||||
|
// project and buffers are now editable
|
||||||
|
assert!(project_b.read_with(cx_b, |project, _| !project.is_read_only()));
|
||||||
|
assert!(editor_b.update(cx_b, |editor, cx| !editor.read_only(cx)));
|
||||||
|
room_b
|
||||||
|
.update(cx_b, |room, cx| room.share_microphone(cx))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// B is demoted
|
||||||
|
active_call_a
|
||||||
|
.update(cx_a, |call, cx| {
|
||||||
|
call.room().unwrap().update(cx, |room, cx| {
|
||||||
|
room.set_participant_role(
|
||||||
|
client_b.user_id().unwrap(),
|
||||||
|
proto::ChannelRole::Guest,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
cx_a.run_until_parked();
|
||||||
|
|
||||||
|
// project and buffers are no longer editable
|
||||||
|
assert!(project_b.read_with(cx_b, |project, _| project.is_read_only()));
|
||||||
|
assert!(editor_b.update(cx_b, |editor, cx| editor.read_only(cx)));
|
||||||
|
assert!(room_b
|
||||||
|
.update(cx_b, |room, cx| room.share_microphone(cx))
|
||||||
|
.await
|
||||||
|
.is_err());
|
||||||
}
|
}
|
||||||
|
|
|
@ -262,7 +262,6 @@ async fn test_remove_channel_message(
|
||||||
|
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
fn assert_messages(chat: &Model<ChannelChat>, messages: &[&str], cx: &mut TestAppContext) {
|
fn assert_messages(chat: &Model<ChannelChat>, messages: &[&str], cx: &mut TestAppContext) {
|
||||||
// todo!(don't directly borrow here)
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
chat.read_with(cx, |chat, _| {
|
chat.read_with(cx, |chat, _| {
|
||||||
chat.messages()
|
chat.messages()
|
||||||
|
|
|
@ -1337,6 +1337,7 @@ async fn test_guest_access(
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
executor.run_until_parked();
|
||||||
|
|
||||||
assert_channels_list_shape(client_b.channel_store(), cx_b, &[]);
|
assert_channels_list_shape(client_b.channel_store(), cx_b, &[]);
|
||||||
|
|
||||||
|
|
|
@ -71,6 +71,7 @@ async fn test_host_disconnect(
|
||||||
let workspace_b =
|
let workspace_b =
|
||||||
cx_b.add_window(|cx| Workspace::new(0, project_b.clone(), client_b.app_state.clone(), cx));
|
cx_b.add_window(|cx| Workspace::new(0, project_b.clone(), client_b.app_state.clone(), cx));
|
||||||
let cx_b = &mut VisualTestContext::from_window(*workspace_b, cx_b);
|
let cx_b = &mut VisualTestContext::from_window(*workspace_b, cx_b);
|
||||||
|
let workspace_b_view = workspace_b.root_view(cx_b).unwrap();
|
||||||
|
|
||||||
let editor_b = workspace_b
|
let editor_b = workspace_b
|
||||||
.update(cx_b, |workspace, cx| {
|
.update(cx_b, |workspace, cx| {
|
||||||
|
@ -85,8 +86,10 @@ async fn test_host_disconnect(
|
||||||
//TODO: focus
|
//TODO: focus
|
||||||
assert!(cx_b.update_view(&editor_b, |editor, cx| editor.is_focused(cx)));
|
assert!(cx_b.update_view(&editor_b, |editor, cx| editor.is_focused(cx)));
|
||||||
editor_b.update(cx_b, |editor, cx| editor.insert("X", cx));
|
editor_b.update(cx_b, |editor, cx| editor.insert("X", cx));
|
||||||
//todo(is_edited)
|
|
||||||
// assert!(workspace_b.is_edited(cx_b));
|
cx_b.update(|cx| {
|
||||||
|
assert!(workspace_b_view.read(cx).is_edited());
|
||||||
|
});
|
||||||
|
|
||||||
// Drop client A's connection. Collaborators should disappear and the project should not be shown as shared.
|
// Drop client A's connection. Collaborators should disappear and the project should not be shown as shared.
|
||||||
server.forbid_connections();
|
server.forbid_connections();
|
||||||
|
@ -105,11 +108,11 @@ async fn test_host_disconnect(
|
||||||
// Ensure client B's edited state is reset and that the whole window is blurred.
|
// Ensure client B's edited state is reset and that the whole window is blurred.
|
||||||
|
|
||||||
workspace_b
|
workspace_b
|
||||||
.update(cx_b, |_, cx| {
|
.update(cx_b, |workspace, cx| {
|
||||||
assert_eq!(cx.focused(), None);
|
assert_eq!(cx.focused(), None);
|
||||||
|
assert!(!workspace.is_edited())
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
// assert!(!workspace_b.is_edited(cx_b));
|
|
||||||
|
|
||||||
// Ensure client B is not prompted to save edits when closing window after disconnecting.
|
// Ensure client B is not prompted to save edits when closing window after disconnecting.
|
||||||
let can_close = workspace_b
|
let can_close = workspace_b
|
||||||
|
|
|
@ -76,6 +76,10 @@ async fn test_basic_following(
|
||||||
let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
|
let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
|
||||||
let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
|
let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
|
||||||
|
|
||||||
|
cx_b.update(|cx| {
|
||||||
|
assert!(cx.is_window_active());
|
||||||
|
});
|
||||||
|
|
||||||
// Client A opens some editors.
|
// Client A opens some editors.
|
||||||
let pane_a = workspace_a.update(cx_a, |workspace, _| workspace.active_pane().clone());
|
let pane_a = workspace_a.update(cx_a, |workspace, _| workspace.active_pane().clone());
|
||||||
let editor_a1 = workspace_a
|
let editor_a1 = workspace_a
|
||||||
|
@ -157,7 +161,6 @@ async fn test_basic_following(
|
||||||
.update(cx_c, |call, cx| call.set_location(Some(&project_c), cx))
|
.update(cx_c, |call, cx| call.set_location(Some(&project_c), cx))
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let weak_project_c = project_c.downgrade();
|
|
||||||
drop(project_c);
|
drop(project_c);
|
||||||
|
|
||||||
// Client C also follows client A.
|
// Client C also follows client A.
|
||||||
|
@ -234,17 +237,16 @@ async fn test_basic_following(
|
||||||
workspace_c.update(cx_c, |workspace, cx| {
|
workspace_c.update(cx_c, |workspace, cx| {
|
||||||
workspace.close_window(&Default::default(), cx);
|
workspace.close_window(&Default::default(), cx);
|
||||||
});
|
});
|
||||||
cx_c.update(|_| {
|
executor.run_until_parked();
|
||||||
drop(workspace_c);
|
|
||||||
});
|
|
||||||
cx_b.executor().run_until_parked();
|
|
||||||
// are you sure you want to leave the call?
|
// are you sure you want to leave the call?
|
||||||
cx_c.simulate_prompt_answer(0);
|
cx_c.simulate_prompt_answer(0);
|
||||||
cx_b.executor().run_until_parked();
|
cx_c.cx.update(|_| {
|
||||||
|
drop(workspace_c);
|
||||||
|
});
|
||||||
executor.run_until_parked();
|
executor.run_until_parked();
|
||||||
|
cx_c.cx.update(|_| {});
|
||||||
|
|
||||||
weak_workspace_c.assert_dropped();
|
weak_workspace_c.assert_dropped();
|
||||||
weak_project_c.assert_dropped();
|
|
||||||
|
|
||||||
// Clients A and B see that client B is following A, and client C is not present in the followers.
|
// Clients A and B see that client B is following A, and client C is not present in the followers.
|
||||||
executor.run_until_parked();
|
executor.run_until_parked();
|
||||||
|
@ -1363,8 +1365,6 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut
|
||||||
let mut server = TestServer::start(executor.clone()).await;
|
let mut server = TestServer::start(executor.clone()).await;
|
||||||
let client_a = server.create_client(cx_a, "user_a").await;
|
let client_a = server.create_client(cx_a, "user_a").await;
|
||||||
let client_b = server.create_client(cx_b, "user_b").await;
|
let client_b = server.create_client(cx_b, "user_b").await;
|
||||||
cx_a.update(editor::init);
|
|
||||||
cx_b.update(editor::init);
|
|
||||||
|
|
||||||
client_a
|
client_a
|
||||||
.fs()
|
.fs()
|
||||||
|
@ -1400,9 +1400,6 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut
|
||||||
let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
|
let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
|
||||||
let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
|
let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
|
||||||
|
|
||||||
cx_a.update(|cx| collab_ui::init(&client_a.app_state, cx));
|
|
||||||
cx_b.update(|cx| collab_ui::init(&client_b.app_state, cx));
|
|
||||||
|
|
||||||
active_call_a
|
active_call_a
|
||||||
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
||||||
.await
|
.await
|
||||||
|
|
|
@ -3065,6 +3065,7 @@ async fn test_local_settings(
|
||||||
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
executor.run_until_parked();
|
||||||
|
|
||||||
// As client B, join that project and observe the local settings.
|
// As client B, join that project and observe the local settings.
|
||||||
let project_b = client_b.build_remote_project(project_id, cx_b).await;
|
let project_b = client_b.build_remote_project(project_id, cx_b).await;
|
||||||
|
@ -4936,10 +4937,10 @@ async fn test_project_symbols(
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
buffer_b_2.read_with(cx_b, |buffer, _| {
|
buffer_b_2.read_with(cx_b, |buffer, cx| {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
buffer.file().unwrap().path().as_ref(),
|
buffer.file().unwrap().full_path(cx),
|
||||||
Path::new("../crate-2/two.rs")
|
Path::new("/code/crate-2/two.rs")
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,11 @@ use node_runtime::FakeNodeRuntime;
|
||||||
use notifications::NotificationStore;
|
use notifications::NotificationStore;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use project::{Project, WorktreeId};
|
use project::{Project, WorktreeId};
|
||||||
use rpc::{proto::ChannelRole, RECEIVE_TIMEOUT};
|
use rpc::{
|
||||||
|
proto::{self, ChannelRole},
|
||||||
|
RECEIVE_TIMEOUT,
|
||||||
|
};
|
||||||
|
use serde_json::json;
|
||||||
use settings::SettingsStore;
|
use settings::SettingsStore;
|
||||||
use std::{
|
use std::{
|
||||||
cell::{Ref, RefCell, RefMut},
|
cell::{Ref, RefCell, RefMut},
|
||||||
|
@ -228,12 +232,16 @@ impl TestServer {
|
||||||
Project::init(&client, cx);
|
Project::init(&client, cx);
|
||||||
client::init(&client, cx);
|
client::init(&client, cx);
|
||||||
language::init(cx);
|
language::init(cx);
|
||||||
editor::init_settings(cx);
|
editor::init(cx);
|
||||||
workspace::init(app_state.clone(), cx);
|
workspace::init(app_state.clone(), cx);
|
||||||
audio::init((), cx);
|
audio::init((), cx);
|
||||||
call::init(client.clone(), user_store.clone(), cx);
|
call::init(client.clone(), user_store.clone(), cx);
|
||||||
channel::init(&client, user_store.clone(), cx);
|
channel::init(&client, user_store.clone(), cx);
|
||||||
notifications::init(client.clone(), user_store, cx);
|
notifications::init(client.clone(), user_store, cx);
|
||||||
|
collab_ui::init(&app_state, cx);
|
||||||
|
file_finder::init(cx);
|
||||||
|
menu::init();
|
||||||
|
settings::KeymapFile::load_asset("keymaps/default.json", cx).unwrap();
|
||||||
});
|
});
|
||||||
|
|
||||||
client
|
client
|
||||||
|
@ -351,6 +359,31 @@ impl TestServer {
|
||||||
channel_id
|
channel_id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn make_public_channel(
|
||||||
|
&self,
|
||||||
|
channel: &str,
|
||||||
|
client: &TestClient,
|
||||||
|
cx: &mut TestAppContext,
|
||||||
|
) -> u64 {
|
||||||
|
let channel_id = self
|
||||||
|
.make_channel(channel, None, (client, cx), &mut [])
|
||||||
|
.await;
|
||||||
|
|
||||||
|
client
|
||||||
|
.channel_store()
|
||||||
|
.update(cx, |channel_store, cx| {
|
||||||
|
channel_store.set_channel_visibility(
|
||||||
|
channel_id,
|
||||||
|
proto::ChannelVisibility::Public,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
channel_id
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn make_channel_tree(
|
pub async fn make_channel_tree(
|
||||||
&self,
|
&self,
|
||||||
channels: &[(&str, Option<&str>)],
|
channels: &[(&str, Option<&str>)],
|
||||||
|
@ -580,6 +613,20 @@ impl TestClient {
|
||||||
(project, worktree.read_with(cx, |tree, _| tree.id()))
|
(project, worktree.read_with(cx, |tree, _| tree.id()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn build_test_project(&self, cx: &mut TestAppContext) -> Model<Project> {
|
||||||
|
self.fs()
|
||||||
|
.insert_tree(
|
||||||
|
"/a",
|
||||||
|
json!({
|
||||||
|
"1.txt": "one\none\none",
|
||||||
|
"2.txt": "two\ntwo\ntwo",
|
||||||
|
"3.txt": "three\nthree\nthree",
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
self.build_local_project("/a", cx).await.0
|
||||||
|
}
|
||||||
|
|
||||||
pub fn build_empty_local_project(&self, cx: &mut TestAppContext) -> Model<Project> {
|
pub fn build_empty_local_project(&self, cx: &mut TestAppContext) -> Model<Project> {
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
Project::local(
|
Project::local(
|
||||||
|
@ -617,7 +664,22 @@ impl TestClient {
|
||||||
project: &Model<Project>,
|
project: &Model<Project>,
|
||||||
cx: &'a mut TestAppContext,
|
cx: &'a mut TestAppContext,
|
||||||
) -> (View<Workspace>, &'a mut VisualTestContext) {
|
) -> (View<Workspace>, &'a mut VisualTestContext) {
|
||||||
cx.add_window_view(|cx| Workspace::new(0, project.clone(), self.app_state.clone(), cx))
|
cx.add_window_view(|cx| {
|
||||||
|
cx.activate_window();
|
||||||
|
Workspace::new(0, project.clone(), self.app_state.clone(), cx)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn active_workspace<'a>(
|
||||||
|
&'a self,
|
||||||
|
cx: &'a mut TestAppContext,
|
||||||
|
) -> (View<Workspace>, &'a mut VisualTestContext) {
|
||||||
|
let window = cx.update(|cx| cx.active_window().unwrap().downcast::<Workspace>().unwrap());
|
||||||
|
|
||||||
|
let view = window.root_view(cx).unwrap();
|
||||||
|
let cx = Box::new(VisualTestContext::from_window(*window.deref(), cx));
|
||||||
|
// it might be nice to try and cleanup these at the end of each test.
|
||||||
|
(view, Box::leak(cx))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,8 @@ path = "src/collab_ui.rs"
|
||||||
doctest = false
|
doctest = false
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
default = []
|
||||||
|
stories = ["dep:story"]
|
||||||
test-support = [
|
test-support = [
|
||||||
"call/test-support",
|
"call/test-support",
|
||||||
"client/test-support",
|
"client/test-support",
|
||||||
|
@ -44,6 +46,7 @@ project = { path = "../project" }
|
||||||
recent_projects = { path = "../recent_projects" }
|
recent_projects = { path = "../recent_projects" }
|
||||||
rpc = { path = "../rpc" }
|
rpc = { path = "../rpc" }
|
||||||
settings = { path = "../settings" }
|
settings = { path = "../settings" }
|
||||||
|
story = { path = "../story", optional = true }
|
||||||
feature_flags = { path = "../feature_flags"}
|
feature_flags = { path = "../feature_flags"}
|
||||||
theme = { path = "../theme" }
|
theme = { path = "../theme" }
|
||||||
theme_selector = { path = "../theme_selector" }
|
theme_selector = { path = "../theme_selector" }
|
||||||
|
|
|
@ -19,9 +19,8 @@ use rich_text::RichText;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use settings::{Settings, SettingsStore};
|
use settings::{Settings, SettingsStore};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use theme::ActiveTheme as _;
|
|
||||||
use time::{OffsetDateTime, UtcOffset};
|
use time::{OffsetDateTime, UtcOffset};
|
||||||
use ui::{prelude::*, Avatar, Button, Icon, IconButton, Label, TabBar, Tooltip};
|
use ui::{prelude::*, Avatar, Button, IconButton, IconName, Label, TabBar, Tooltip};
|
||||||
use util::{ResultExt, TryFutureExt};
|
use util::{ResultExt, TryFutureExt};
|
||||||
use workspace::{
|
use workspace::{
|
||||||
dock::{DockPosition, Panel, PanelEvent},
|
dock::{DockPosition, Panel, PanelEvent},
|
||||||
|
@ -48,7 +47,7 @@ pub struct ChatPanel {
|
||||||
languages: Arc<LanguageRegistry>,
|
languages: Arc<LanguageRegistry>,
|
||||||
message_list: ListState,
|
message_list: ListState,
|
||||||
active_chat: Option<(Model<ChannelChat>, Subscription)>,
|
active_chat: Option<(Model<ChannelChat>, Subscription)>,
|
||||||
input_editor: View<MessageEditor>,
|
message_editor: View<MessageEditor>,
|
||||||
local_timezone: UtcOffset,
|
local_timezone: UtcOffset,
|
||||||
fs: Arc<dyn Fs>,
|
fs: Arc<dyn Fs>,
|
||||||
width: Option<Pixels>,
|
width: Option<Pixels>,
|
||||||
|
@ -120,7 +119,7 @@ impl ChatPanel {
|
||||||
message_list,
|
message_list,
|
||||||
active_chat: Default::default(),
|
active_chat: Default::default(),
|
||||||
pending_serialization: Task::ready(None),
|
pending_serialization: Task::ready(None),
|
||||||
input_editor,
|
message_editor: input_editor,
|
||||||
local_timezone: cx.local_timezone(),
|
local_timezone: cx.local_timezone(),
|
||||||
subscriptions: Vec::new(),
|
subscriptions: Vec::new(),
|
||||||
workspace: workspace_handle,
|
workspace: workspace_handle,
|
||||||
|
@ -209,7 +208,7 @@ impl ChatPanel {
|
||||||
self.message_list.reset(chat.message_count());
|
self.message_list.reset(chat.message_count());
|
||||||
|
|
||||||
let channel_name = chat.channel(cx).map(|channel| channel.name.clone());
|
let channel_name = chat.channel(cx).map(|channel| channel.name.clone());
|
||||||
self.input_editor.update(cx, |editor, cx| {
|
self.message_editor.update(cx, |editor, cx| {
|
||||||
editor.set_channel(channel_id, channel_name, cx);
|
editor.set_channel(channel_id, channel_name, cx);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -282,12 +281,12 @@ impl ChatPanel {
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
.end_child(
|
.end_child(
|
||||||
IconButton::new("notes", Icon::File)
|
IconButton::new("notes", IconName::File)
|
||||||
.on_click(cx.listener(Self::open_notes))
|
.on_click(cx.listener(Self::open_notes))
|
||||||
.tooltip(|cx| Tooltip::text("Open notes", cx)),
|
.tooltip(|cx| Tooltip::text("Open notes", cx)),
|
||||||
)
|
)
|
||||||
.end_child(
|
.end_child(
|
||||||
IconButton::new("call", Icon::AudioOn)
|
IconButton::new("call", IconName::AudioOn)
|
||||||
.on_click(cx.listener(Self::join_call))
|
.on_click(cx.listener(Self::join_call))
|
||||||
.tooltip(|cx| Tooltip::text("Join call", cx)),
|
.tooltip(|cx| Tooltip::text("Join call", cx)),
|
||||||
),
|
),
|
||||||
|
@ -300,13 +299,7 @@ impl ChatPanel {
|
||||||
this
|
this
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
.child(
|
.child(h_stack().p_2().child(self.message_editor.clone()))
|
||||||
div()
|
|
||||||
.z_index(1)
|
|
||||||
.p_2()
|
|
||||||
.bg(cx.theme().colors().background)
|
|
||||||
.child(self.input_editor.clone()),
|
|
||||||
)
|
|
||||||
.into_any()
|
.into_any()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -402,7 +395,7 @@ impl ChatPanel {
|
||||||
.w_8()
|
.w_8()
|
||||||
.visible_on_hover("")
|
.visible_on_hover("")
|
||||||
.children(message_id_to_remove.map(|message_id| {
|
.children(message_id_to_remove.map(|message_id| {
|
||||||
IconButton::new(("remove", message_id), Icon::XCircle).on_click(
|
IconButton::new(("remove", message_id), IconName::XCircle).on_click(
|
||||||
cx.listener(move |this, _, cx| {
|
cx.listener(move |this, _, cx| {
|
||||||
this.remove_message(message_id, cx);
|
this.remove_message(message_id, cx);
|
||||||
}),
|
}),
|
||||||
|
@ -436,7 +429,7 @@ impl ChatPanel {
|
||||||
Button::new("sign-in", "Sign in")
|
Button::new("sign-in", "Sign in")
|
||||||
.style(ButtonStyle::Filled)
|
.style(ButtonStyle::Filled)
|
||||||
.icon_color(Color::Muted)
|
.icon_color(Color::Muted)
|
||||||
.icon(Icon::Github)
|
.icon(IconName::Github)
|
||||||
.icon_position(IconPosition::Start)
|
.icon_position(IconPosition::Start)
|
||||||
.full_width()
|
.full_width()
|
||||||
.on_click(cx.listener(move |this, _, cx| {
|
.on_click(cx.listener(move |this, _, cx| {
|
||||||
|
@ -469,7 +462,7 @@ impl ChatPanel {
|
||||||
fn send(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
|
fn send(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
|
||||||
if let Some((chat, _)) = self.active_chat.as_ref() {
|
if let Some((chat, _)) = self.active_chat.as_ref() {
|
||||||
let message = self
|
let message = self
|
||||||
.input_editor
|
.message_editor
|
||||||
.update(cx, |editor, cx| editor.take_message(cx));
|
.update(cx, |editor, cx| editor.take_message(cx));
|
||||||
|
|
||||||
if let Some(task) = chat
|
if let Some(task) = chat
|
||||||
|
@ -585,7 +578,7 @@ impl Render for ChatPanel {
|
||||||
|
|
||||||
impl FocusableView for ChatPanel {
|
impl FocusableView for ChatPanel {
|
||||||
fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
|
fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
|
||||||
self.input_editor.read(cx).focus_handle(cx)
|
self.message_editor.read(cx).focus_handle(cx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -629,12 +622,12 @@ impl Panel for ChatPanel {
|
||||||
"ChatPanel"
|
"ChatPanel"
|
||||||
}
|
}
|
||||||
|
|
||||||
fn icon(&self, cx: &WindowContext) -> Option<ui::Icon> {
|
fn icon(&self, cx: &WindowContext) -> Option<ui::IconName> {
|
||||||
if !is_channels_feature_enabled(cx) {
|
if !is_channels_feature_enabled(cx) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(ui::Icon::MessageBubbles).filter(|_| ChatPanelSettings::get_global(cx).button)
|
Some(ui::IconName::MessageBubbles).filter(|_| ChatPanelSettings::get_global(cx).button)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
|
fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
|
||||||
|
|
|
@ -1,16 +1,19 @@
|
||||||
|
use std::{sync::Arc, time::Duration};
|
||||||
|
|
||||||
use channel::{ChannelId, ChannelMembership, ChannelStore, MessageParams};
|
use channel::{ChannelId, ChannelMembership, ChannelStore, MessageParams};
|
||||||
use client::UserId;
|
use client::UserId;
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use editor::{AnchorRangeExt, Editor};
|
use editor::{AnchorRangeExt, Editor, EditorElement, EditorStyle};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AsyncWindowContext, FocusableView, IntoElement, Model, Render, SharedString, Task, View,
|
AsyncWindowContext, FocusableView, FontStyle, FontWeight, HighlightStyle, IntoElement, Model,
|
||||||
ViewContext, WeakView,
|
Render, SharedString, Task, TextStyle, View, ViewContext, WeakView, WhiteSpace,
|
||||||
};
|
};
|
||||||
use language::{language_settings::SoftWrap, Buffer, BufferSnapshot, LanguageRegistry};
|
use language::{language_settings::SoftWrap, Buffer, BufferSnapshot, LanguageRegistry};
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use project::search::SearchQuery;
|
use project::search::SearchQuery;
|
||||||
use std::{sync::Arc, time::Duration};
|
use settings::Settings;
|
||||||
use workspace::item::ItemHandle;
|
use theme::ThemeSettings;
|
||||||
|
use ui::prelude::*;
|
||||||
|
|
||||||
const MENTIONS_DEBOUNCE_INTERVAL: Duration = Duration::from_millis(50);
|
const MENTIONS_DEBOUNCE_INTERVAL: Duration = Duration::from_millis(50);
|
||||||
|
|
||||||
|
@ -181,7 +184,14 @@ impl MessageEditor {
|
||||||
}
|
}
|
||||||
|
|
||||||
editor.clear_highlights::<Self>(cx);
|
editor.clear_highlights::<Self>(cx);
|
||||||
editor.highlight_text::<Self>(anchor_ranges, gpui::red().into(), cx)
|
editor.highlight_text::<Self>(
|
||||||
|
anchor_ranges,
|
||||||
|
HighlightStyle {
|
||||||
|
font_weight: Some(FontWeight::BOLD),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
cx,
|
||||||
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
this.mentions = mentioned_user_ids;
|
this.mentions = mentioned_user_ids;
|
||||||
|
@ -196,8 +206,39 @@ impl MessageEditor {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Render for MessageEditor {
|
impl Render for MessageEditor {
|
||||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
self.editor.to_any()
|
let settings = ThemeSettings::get_global(cx);
|
||||||
|
let text_style = TextStyle {
|
||||||
|
color: if self.editor.read(cx).read_only(cx) {
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
|
||||||
|
div()
|
||||||
|
.w_full()
|
||||||
|
.px_2()
|
||||||
|
.py_1()
|
||||||
|
.bg(cx.theme().colors().editor_background)
|
||||||
|
.rounded_md()
|
||||||
|
.child(EditorElement::new(
|
||||||
|
&self.editor,
|
||||||
|
EditorStyle {
|
||||||
|
local_player: cx.theme().players().local(),
|
||||||
|
text: text_style,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -205,7 +246,7 @@ impl Render for MessageEditor {
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use client::{Client, User, UserStore};
|
use client::{Client, User, UserStore};
|
||||||
use gpui::{Context as _, TestAppContext, VisualContext as _};
|
use gpui::TestAppContext;
|
||||||
use language::{Language, LanguageConfig};
|
use language::{Language, LanguageConfig};
|
||||||
use rpc::proto;
|
use rpc::proto;
|
||||||
use settings::SettingsStore;
|
use settings::SettingsStore;
|
||||||
|
|
|
@ -31,13 +31,13 @@ use smallvec::SmallVec;
|
||||||
use std::{mem, sync::Arc};
|
use std::{mem, sync::Arc};
|
||||||
use theme::{ActiveTheme, ThemeSettings};
|
use theme::{ActiveTheme, ThemeSettings};
|
||||||
use ui::{
|
use ui::{
|
||||||
prelude::*, Avatar, Button, Color, ContextMenu, Icon, IconButton, IconElement, IconSize, Label,
|
prelude::*, Avatar, Button, Color, ContextMenu, Icon, IconButton, IconName, IconSize, Label,
|
||||||
ListHeader, ListItem, Tooltip,
|
ListHeader, ListItem, Tooltip,
|
||||||
};
|
};
|
||||||
use util::{maybe, ResultExt, TryFutureExt};
|
use util::{maybe, ResultExt, TryFutureExt};
|
||||||
use workspace::{
|
use workspace::{
|
||||||
dock::{DockPosition, Panel, PanelEvent},
|
dock::{DockPosition, Panel, PanelEvent},
|
||||||
notifications::NotifyResultExt,
|
notifications::{NotifyResultExt, NotifyTaskExt},
|
||||||
Workspace,
|
Workspace,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -140,6 +140,7 @@ enum ListEntry {
|
||||||
user: Arc<User>,
|
user: Arc<User>,
|
||||||
peer_id: Option<PeerId>,
|
peer_id: Option<PeerId>,
|
||||||
is_pending: bool,
|
is_pending: bool,
|
||||||
|
role: proto::ChannelRole,
|
||||||
},
|
},
|
||||||
ParticipantProject {
|
ParticipantProject {
|
||||||
project_id: u64,
|
project_id: u64,
|
||||||
|
@ -151,10 +152,6 @@ enum ListEntry {
|
||||||
peer_id: Option<PeerId>,
|
peer_id: Option<PeerId>,
|
||||||
is_last: bool,
|
is_last: bool,
|
||||||
},
|
},
|
||||||
GuestCount {
|
|
||||||
count: usize,
|
|
||||||
has_visible_participants: bool,
|
|
||||||
},
|
|
||||||
IncomingRequest(Arc<User>),
|
IncomingRequest(Arc<User>),
|
||||||
OutgoingRequest(Arc<User>),
|
OutgoingRequest(Arc<User>),
|
||||||
ChannelInvite(Arc<Channel>),
|
ChannelInvite(Arc<Channel>),
|
||||||
|
@ -384,14 +381,10 @@ impl CollabPanel {
|
||||||
|
|
||||||
if !self.collapsed_sections.contains(&Section::ActiveCall) {
|
if !self.collapsed_sections.contains(&Section::ActiveCall) {
|
||||||
let room = room.read(cx);
|
let room = room.read(cx);
|
||||||
let mut guest_count_ix = 0;
|
|
||||||
let mut guest_count = if room.read_only() { 1 } else { 0 };
|
|
||||||
let mut non_guest_count = if room.read_only() { 0 } else { 1 };
|
|
||||||
|
|
||||||
if let Some(channel_id) = room.channel_id() {
|
if let Some(channel_id) = room.channel_id() {
|
||||||
self.entries.push(ListEntry::ChannelNotes { channel_id });
|
self.entries.push(ListEntry::ChannelNotes { channel_id });
|
||||||
self.entries.push(ListEntry::ChannelChat { channel_id });
|
self.entries.push(ListEntry::ChannelChat { channel_id });
|
||||||
guest_count_ix = self.entries.len();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Populate the active user.
|
// Populate the active user.
|
||||||
|
@ -410,12 +403,13 @@ impl CollabPanel {
|
||||||
&Default::default(),
|
&Default::default(),
|
||||||
executor.clone(),
|
executor.clone(),
|
||||||
));
|
));
|
||||||
if !matches.is_empty() && !room.read_only() {
|
if !matches.is_empty() {
|
||||||
let user_id = user.id;
|
let user_id = user.id;
|
||||||
self.entries.push(ListEntry::CallParticipant {
|
self.entries.push(ListEntry::CallParticipant {
|
||||||
user,
|
user,
|
||||||
peer_id: None,
|
peer_id: None,
|
||||||
is_pending: false,
|
is_pending: false,
|
||||||
|
role: room.local_participant().role,
|
||||||
});
|
});
|
||||||
let mut projects = room.local_participant().projects.iter().peekable();
|
let mut projects = room.local_participant().projects.iter().peekable();
|
||||||
while let Some(project) = projects.next() {
|
while let Some(project) = projects.next() {
|
||||||
|
@ -442,12 +436,6 @@ impl CollabPanel {
|
||||||
room.remote_participants()
|
room.remote_participants()
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|(_, participant)| {
|
.filter_map(|(_, participant)| {
|
||||||
if participant.role == proto::ChannelRole::Guest {
|
|
||||||
guest_count += 1;
|
|
||||||
return None;
|
|
||||||
} else {
|
|
||||||
non_guest_count += 1;
|
|
||||||
}
|
|
||||||
Some(StringMatchCandidate {
|
Some(StringMatchCandidate {
|
||||||
id: participant.user.id as usize,
|
id: participant.user.id as usize,
|
||||||
string: participant.user.github_login.clone(),
|
string: participant.user.github_login.clone(),
|
||||||
|
@ -455,7 +443,7 @@ impl CollabPanel {
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
let matches = executor.block(match_strings(
|
let mut matches = executor.block(match_strings(
|
||||||
&self.match_candidates,
|
&self.match_candidates,
|
||||||
&query,
|
&query,
|
||||||
true,
|
true,
|
||||||
|
@ -463,6 +451,15 @@ impl CollabPanel {
|
||||||
&Default::default(),
|
&Default::default(),
|
||||||
executor.clone(),
|
executor.clone(),
|
||||||
));
|
));
|
||||||
|
matches.sort_by(|a, b| {
|
||||||
|
let a_is_guest = room.role_for_user(a.candidate_id as u64)
|
||||||
|
== Some(proto::ChannelRole::Guest);
|
||||||
|
let b_is_guest = room.role_for_user(b.candidate_id as u64)
|
||||||
|
== Some(proto::ChannelRole::Guest);
|
||||||
|
a_is_guest
|
||||||
|
.cmp(&b_is_guest)
|
||||||
|
.then_with(|| a.string.cmp(&b.string))
|
||||||
|
});
|
||||||
for mat in matches {
|
for mat in matches {
|
||||||
let user_id = mat.candidate_id as u64;
|
let user_id = mat.candidate_id as u64;
|
||||||
let participant = &room.remote_participants()[&user_id];
|
let participant = &room.remote_participants()[&user_id];
|
||||||
|
@ -470,6 +467,7 @@ impl CollabPanel {
|
||||||
user: participant.user.clone(),
|
user: participant.user.clone(),
|
||||||
peer_id: Some(participant.peer_id),
|
peer_id: Some(participant.peer_id),
|
||||||
is_pending: false,
|
is_pending: false,
|
||||||
|
role: participant.role,
|
||||||
});
|
});
|
||||||
let mut projects = participant.projects.iter().peekable();
|
let mut projects = participant.projects.iter().peekable();
|
||||||
while let Some(project) = projects.next() {
|
while let Some(project) = projects.next() {
|
||||||
|
@ -488,15 +486,6 @@ impl CollabPanel {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if guest_count > 0 {
|
|
||||||
self.entries.insert(
|
|
||||||
guest_count_ix,
|
|
||||||
ListEntry::GuestCount {
|
|
||||||
count: guest_count,
|
|
||||||
has_visible_participants: non_guest_count > 0,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Populate pending participants.
|
// Populate pending participants.
|
||||||
self.match_candidates.clear();
|
self.match_candidates.clear();
|
||||||
|
@ -521,6 +510,7 @@ impl CollabPanel {
|
||||||
user: room.pending_participants()[mat.candidate_id].clone(),
|
user: room.pending_participants()[mat.candidate_id].clone(),
|
||||||
peer_id: None,
|
peer_id: None,
|
||||||
is_pending: true,
|
is_pending: true,
|
||||||
|
role: proto::ChannelRole::Member,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -834,13 +824,19 @@ impl CollabPanel {
|
||||||
user: &Arc<User>,
|
user: &Arc<User>,
|
||||||
peer_id: Option<PeerId>,
|
peer_id: Option<PeerId>,
|
||||||
is_pending: bool,
|
is_pending: bool,
|
||||||
|
role: proto::ChannelRole,
|
||||||
is_selected: bool,
|
is_selected: bool,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) -> ListItem {
|
) -> ListItem {
|
||||||
|
let user_id = user.id;
|
||||||
let is_current_user =
|
let is_current_user =
|
||||||
self.user_store.read(cx).current_user().map(|user| user.id) == Some(user.id);
|
self.user_store.read(cx).current_user().map(|user| user.id) == Some(user_id);
|
||||||
let tooltip = format!("Follow {}", user.github_login);
|
let tooltip = format!("Follow {}", user.github_login);
|
||||||
|
|
||||||
|
let is_call_admin = ActiveCall::global(cx).read(cx).room().is_some_and(|room| {
|
||||||
|
room.read(cx).local_participant().role == proto::ChannelRole::Admin
|
||||||
|
});
|
||||||
|
|
||||||
ListItem::new(SharedString::from(user.github_login.clone()))
|
ListItem::new(SharedString::from(user.github_login.clone()))
|
||||||
.start_slot(Avatar::new(user.avatar_uri.clone()))
|
.start_slot(Avatar::new(user.avatar_uri.clone()))
|
||||||
.child(Label::new(user.github_login.clone()))
|
.child(Label::new(user.github_login.clone()))
|
||||||
|
@ -848,22 +844,32 @@ impl CollabPanel {
|
||||||
.end_slot(if is_pending {
|
.end_slot(if is_pending {
|
||||||
Label::new("Calling").color(Color::Muted).into_any_element()
|
Label::new("Calling").color(Color::Muted).into_any_element()
|
||||||
} else if is_current_user {
|
} else if is_current_user {
|
||||||
IconButton::new("leave-call", Icon::Exit)
|
IconButton::new("leave-call", IconName::Exit)
|
||||||
.style(ButtonStyle::Subtle)
|
.style(ButtonStyle::Subtle)
|
||||||
.on_click(move |_, cx| Self::leave_call(cx))
|
.on_click(move |_, cx| Self::leave_call(cx))
|
||||||
.tooltip(|cx| Tooltip::text("Leave Call", cx))
|
.tooltip(|cx| Tooltip::text("Leave Call", cx))
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
|
} else if role == proto::ChannelRole::Guest {
|
||||||
|
Label::new("Guest").color(Color::Muted).into_any_element()
|
||||||
} else {
|
} else {
|
||||||
div().into_any_element()
|
div().into_any_element()
|
||||||
})
|
})
|
||||||
.when_some(peer_id, |this, peer_id| {
|
.when_some(peer_id, |el, peer_id| {
|
||||||
this.tooltip(move |cx| Tooltip::text(tooltip.clone(), cx))
|
if role == proto::ChannelRole::Guest {
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
el.tooltip(move |cx| Tooltip::text(tooltip.clone(), cx))
|
||||||
.on_click(cx.listener(move |this, _, cx| {
|
.on_click(cx.listener(move |this, _, cx| {
|
||||||
this.workspace
|
this.workspace
|
||||||
.update(cx, |workspace, cx| workspace.follow(peer_id, cx))
|
.update(cx, |workspace, cx| workspace.follow(peer_id, cx))
|
||||||
.ok();
|
.ok();
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
|
.when(is_call_admin, |el| {
|
||||||
|
el.on_secondary_mouse_down(cx.listener(move |this, event: &MouseDownEvent, cx| {
|
||||||
|
this.deploy_participant_context_menu(event.position, user_id, role, cx)
|
||||||
|
}))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_participant_project(
|
fn render_participant_project(
|
||||||
|
@ -897,7 +903,7 @@ impl CollabPanel {
|
||||||
h_stack()
|
h_stack()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.child(render_tree_branch(is_last, false, cx))
|
.child(render_tree_branch(is_last, false, cx))
|
||||||
.child(IconButton::new(0, Icon::Folder)),
|
.child(IconButton::new(0, IconName::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))
|
||||||
|
@ -918,7 +924,7 @@ impl CollabPanel {
|
||||||
h_stack()
|
h_stack()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.child(render_tree_branch(is_last, false, cx))
|
.child(render_tree_branch(is_last, false, cx))
|
||||||
.child(IconButton::new(0, Icon::Screen)),
|
.child(IconButton::new(0, IconName::Screen)),
|
||||||
)
|
)
|
||||||
.child(Label::new("Screen"))
|
.child(Label::new("Screen"))
|
||||||
.when_some(peer_id, |this, _| {
|
.when_some(peer_id, |this, _| {
|
||||||
|
@ -959,7 +965,7 @@ impl CollabPanel {
|
||||||
h_stack()
|
h_stack()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.child(render_tree_branch(false, true, cx))
|
.child(render_tree_branch(false, true, cx))
|
||||||
.child(IconButton::new(0, Icon::File)),
|
.child(IconButton::new(0, IconName::File)),
|
||||||
)
|
)
|
||||||
.child(div().h_7().w_full().child(Label::new("notes")))
|
.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))
|
||||||
|
@ -980,47 +986,12 @@ impl CollabPanel {
|
||||||
h_stack()
|
h_stack()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.child(render_tree_branch(false, false, cx))
|
.child(render_tree_branch(false, false, cx))
|
||||||
.child(IconButton::new(0, Icon::MessageBubbles)),
|
.child(IconButton::new(0, IconName::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))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_guest_count(
|
|
||||||
&self,
|
|
||||||
count: usize,
|
|
||||||
has_visible_participants: bool,
|
|
||||||
is_selected: bool,
|
|
||||||
cx: &mut ViewContext<Self>,
|
|
||||||
) -> impl IntoElement {
|
|
||||||
let manageable_channel_id = ActiveCall::global(cx).read(cx).room().and_then(|room| {
|
|
||||||
let room = room.read(cx);
|
|
||||||
if room.local_participant_is_admin() {
|
|
||||||
room.channel_id()
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ListItem::new("guest_count")
|
|
||||||
.selected(is_selected)
|
|
||||||
.start_slot(
|
|
||||||
h_stack()
|
|
||||||
.gap_1()
|
|
||||||
.child(render_tree_branch(!has_visible_participants, false, cx))
|
|
||||||
.child(""),
|
|
||||||
)
|
|
||||||
.child(Label::new(if count == 1 {
|
|
||||||
format!("{} guest", count)
|
|
||||||
} else {
|
|
||||||
format!("{} guests", count)
|
|
||||||
}))
|
|
||||||
.when_some(manageable_channel_id, |el, channel_id| {
|
|
||||||
el.tooltip(move |cx| Tooltip::text("Manage Members", cx))
|
|
||||||
.on_click(cx.listener(move |this, _, cx| this.manage_members(channel_id, cx)))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn has_subchannels(&self, ix: usize) -> bool {
|
fn has_subchannels(&self, ix: usize) -> bool {
|
||||||
self.entries.get(ix).map_or(false, |entry| {
|
self.entries.get(ix).map_or(false, |entry| {
|
||||||
if let ListEntry::Channel { has_children, .. } = entry {
|
if let ListEntry::Channel { has_children, .. } = entry {
|
||||||
|
@ -1031,6 +1002,80 @@ impl CollabPanel {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn deploy_participant_context_menu(
|
||||||
|
&mut self,
|
||||||
|
position: Point<Pixels>,
|
||||||
|
user_id: u64,
|
||||||
|
role: proto::ChannelRole,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) {
|
||||||
|
let this = cx.view().clone();
|
||||||
|
if !(role == proto::ChannelRole::Guest || role == proto::ChannelRole::Member) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let context_menu = ContextMenu::build(cx, |context_menu, cx| {
|
||||||
|
if role == proto::ChannelRole::Guest {
|
||||||
|
context_menu.entry(
|
||||||
|
"Grant Write Access",
|
||||||
|
None,
|
||||||
|
cx.handler_for(&this, move |_, cx| {
|
||||||
|
ActiveCall::global(cx)
|
||||||
|
.update(cx, |call, cx| {
|
||||||
|
let Some(room) = call.room() else {
|
||||||
|
return Task::ready(Ok(()));
|
||||||
|
};
|
||||||
|
room.update(cx, |room, cx| {
|
||||||
|
room.set_participant_role(
|
||||||
|
user_id,
|
||||||
|
proto::ChannelRole::Member,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.detach_and_notify_err(cx)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
} else if role == proto::ChannelRole::Member {
|
||||||
|
context_menu.entry(
|
||||||
|
"Revoke Write Access",
|
||||||
|
None,
|
||||||
|
cx.handler_for(&this, move |_, cx| {
|
||||||
|
ActiveCall::global(cx)
|
||||||
|
.update(cx, |call, cx| {
|
||||||
|
let Some(room) = call.room() else {
|
||||||
|
return Task::ready(Ok(()));
|
||||||
|
};
|
||||||
|
room.update(cx, |room, cx| {
|
||||||
|
room.set_participant_role(
|
||||||
|
user_id,
|
||||||
|
proto::ChannelRole::Guest,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.detach_and_notify_err(cx)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.focus_view(&context_menu);
|
||||||
|
let subscription =
|
||||||
|
cx.subscribe(&context_menu, |this, _, _: &DismissEvent, cx| {
|
||||||
|
if this.context_menu.as_ref().is_some_and(|context_menu| {
|
||||||
|
context_menu.0.focus_handle(cx).contains_focused(cx)
|
||||||
|
}) {
|
||||||
|
cx.focus_self();
|
||||||
|
}
|
||||||
|
this.context_menu.take();
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
self.context_menu = Some((context_menu, position, subscription));
|
||||||
|
}
|
||||||
|
|
||||||
fn deploy_channel_context_menu(
|
fn deploy_channel_context_menu(
|
||||||
&mut self,
|
&mut self,
|
||||||
position: Point<Pixels>,
|
position: Point<Pixels>,
|
||||||
|
@ -1242,18 +1287,6 @@ impl CollabPanel {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ListEntry::GuestCount { .. } => {
|
|
||||||
let Some(room) = ActiveCall::global(cx).read(cx).room() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let room = room.read(cx);
|
|
||||||
let Some(channel_id) = room.channel_id() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
if room.local_participant_is_admin() {
|
|
||||||
self.manage_members(channel_id, cx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ListEntry::Channel { channel, .. } => {
|
ListEntry::Channel { channel, .. } => {
|
||||||
let is_active = maybe!({
|
let is_active = maybe!({
|
||||||
let call_channel = ActiveCall::global(cx)
|
let call_channel = ActiveCall::global(cx)
|
||||||
|
@ -1724,7 +1757,7 @@ impl CollabPanel {
|
||||||
.child(
|
.child(
|
||||||
Button::new("sign_in", "Sign in")
|
Button::new("sign_in", "Sign in")
|
||||||
.icon_color(Color::Muted)
|
.icon_color(Color::Muted)
|
||||||
.icon(Icon::Github)
|
.icon(IconName::Github)
|
||||||
.icon_position(IconPosition::Start)
|
.icon_position(IconPosition::Start)
|
||||||
.style(ButtonStyle::Filled)
|
.style(ButtonStyle::Filled)
|
||||||
.full_width()
|
.full_width()
|
||||||
|
@ -1788,8 +1821,9 @@ impl CollabPanel {
|
||||||
user,
|
user,
|
||||||
peer_id,
|
peer_id,
|
||||||
is_pending,
|
is_pending,
|
||||||
|
role,
|
||||||
} => self
|
} => self
|
||||||
.render_call_participant(user, *peer_id, *is_pending, is_selected, cx)
|
.render_call_participant(user, *peer_id, *is_pending, *role, is_selected, cx)
|
||||||
.into_any_element(),
|
.into_any_element(),
|
||||||
ListEntry::ParticipantProject {
|
ListEntry::ParticipantProject {
|
||||||
project_id,
|
project_id,
|
||||||
|
@ -1809,12 +1843,6 @@ impl CollabPanel {
|
||||||
ListEntry::ParticipantScreen { peer_id, is_last } => self
|
ListEntry::ParticipantScreen { peer_id, is_last } => self
|
||||||
.render_participant_screen(*peer_id, *is_last, is_selected, cx)
|
.render_participant_screen(*peer_id, *is_last, is_selected, cx)
|
||||||
.into_any_element(),
|
.into_any_element(),
|
||||||
ListEntry::GuestCount {
|
|
||||||
count,
|
|
||||||
has_visible_participants,
|
|
||||||
} => self
|
|
||||||
.render_guest_count(*count, *has_visible_participants, is_selected, cx)
|
|
||||||
.into_any_element(),
|
|
||||||
ListEntry::ChannelNotes { channel_id } => self
|
ListEntry::ChannelNotes { channel_id } => self
|
||||||
.render_channel_notes(*channel_id, is_selected, cx)
|
.render_channel_notes(*channel_id, is_selected, cx)
|
||||||
.into_any_element(),
|
.into_any_element(),
|
||||||
|
@ -1921,7 +1949,7 @@ impl CollabPanel {
|
||||||
let button = match section {
|
let button = match section {
|
||||||
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", IconName::Copy)
|
||||||
.icon_size(IconSize::Small)
|
.icon_size(IconSize::Small)
|
||||||
.size(ButtonSize::None)
|
.size(ButtonSize::None)
|
||||||
.visible_on_hover("section-header")
|
.visible_on_hover("section-header")
|
||||||
|
@ -1933,13 +1961,13 @@ impl CollabPanel {
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}),
|
}),
|
||||||
Section::Contacts => Some(
|
Section::Contacts => Some(
|
||||||
IconButton::new("add-contact", Icon::Plus)
|
IconButton::new("add-contact", IconName::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(),
|
.into_any_element(),
|
||||||
),
|
),
|
||||||
Section::Channels => Some(
|
Section::Channels => Some(
|
||||||
IconButton::new("add-channel", Icon::Plus)
|
IconButton::new("add-channel", IconName::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(),
|
.into_any_element(),
|
||||||
|
@ -2010,7 +2038,7 @@ impl CollabPanel {
|
||||||
})
|
})
|
||||||
.when(!calling, |el| {
|
.when(!calling, |el| {
|
||||||
el.child(
|
el.child(
|
||||||
IconButton::new("remove_contact", Icon::Close)
|
IconButton::new("remove_contact", IconName::Close)
|
||||||
.icon_color(Color::Muted)
|
.icon_color(Color::Muted)
|
||||||
.visible_on_hover("")
|
.visible_on_hover("")
|
||||||
.tooltip(|cx| Tooltip::text("Remove Contact", cx))
|
.tooltip(|cx| Tooltip::text("Remove Contact", cx))
|
||||||
|
@ -2071,13 +2099,13 @@ impl CollabPanel {
|
||||||
|
|
||||||
let controls = if is_incoming {
|
let controls = if is_incoming {
|
||||||
vec![
|
vec![
|
||||||
IconButton::new("decline-contact", Icon::Close)
|
IconButton::new("decline-contact", IconName::Close)
|
||||||
.on_click(cx.listener(move |this, _, cx| {
|
.on_click(cx.listener(move |this, _, cx| {
|
||||||
this.respond_to_contact_request(user_id, false, cx);
|
this.respond_to_contact_request(user_id, false, cx);
|
||||||
}))
|
}))
|
||||||
.icon_color(color)
|
.icon_color(color)
|
||||||
.tooltip(|cx| Tooltip::text("Decline invite", cx)),
|
.tooltip(|cx| Tooltip::text("Decline invite", cx)),
|
||||||
IconButton::new("accept-contact", Icon::Check)
|
IconButton::new("accept-contact", IconName::Check)
|
||||||
.on_click(cx.listener(move |this, _, cx| {
|
.on_click(cx.listener(move |this, _, cx| {
|
||||||
this.respond_to_contact_request(user_id, true, cx);
|
this.respond_to_contact_request(user_id, true, cx);
|
||||||
}))
|
}))
|
||||||
|
@ -2086,7 +2114,7 @@ impl CollabPanel {
|
||||||
]
|
]
|
||||||
} else {
|
} else {
|
||||||
let github_login = github_login.clone();
|
let github_login = github_login.clone();
|
||||||
vec![IconButton::new("remove_contact", Icon::Close)
|
vec![IconButton::new("remove_contact", IconName::Close)
|
||||||
.on_click(cx.listener(move |this, _, cx| {
|
.on_click(cx.listener(move |this, _, cx| {
|
||||||
this.remove_contact(user_id, &github_login, cx);
|
this.remove_contact(user_id, &github_login, cx);
|
||||||
}))
|
}))
|
||||||
|
@ -2126,13 +2154,13 @@ impl CollabPanel {
|
||||||
};
|
};
|
||||||
|
|
||||||
let controls = [
|
let controls = [
|
||||||
IconButton::new("reject-invite", Icon::Close)
|
IconButton::new("reject-invite", IconName::Close)
|
||||||
.on_click(cx.listener(move |this, _, cx| {
|
.on_click(cx.listener(move |this, _, cx| {
|
||||||
this.respond_to_channel_invite(channel_id, false, cx);
|
this.respond_to_channel_invite(channel_id, false, cx);
|
||||||
}))
|
}))
|
||||||
.icon_color(color)
|
.icon_color(color)
|
||||||
.tooltip(|cx| Tooltip::text("Decline invite", cx)),
|
.tooltip(|cx| Tooltip::text("Decline invite", cx)),
|
||||||
IconButton::new("accept-invite", Icon::Check)
|
IconButton::new("accept-invite", IconName::Check)
|
||||||
.on_click(cx.listener(move |this, _, cx| {
|
.on_click(cx.listener(move |this, _, cx| {
|
||||||
this.respond_to_channel_invite(channel_id, true, cx);
|
this.respond_to_channel_invite(channel_id, true, cx);
|
||||||
}))
|
}))
|
||||||
|
@ -2150,7 +2178,7 @@ impl CollabPanel {
|
||||||
.child(h_stack().children(controls)),
|
.child(h_stack().children(controls)),
|
||||||
)
|
)
|
||||||
.start_slot(
|
.start_slot(
|
||||||
IconElement::new(Icon::Hash)
|
Icon::new(IconName::Hash)
|
||||||
.size(IconSize::Small)
|
.size(IconSize::Small)
|
||||||
.color(Color::Muted),
|
.color(Color::Muted),
|
||||||
)
|
)
|
||||||
|
@ -2162,7 +2190,7 @@ impl CollabPanel {
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) -> ListItem {
|
) -> ListItem {
|
||||||
ListItem::new("contact-placeholder")
|
ListItem::new("contact-placeholder")
|
||||||
.child(IconElement::new(Icon::Plus))
|
.child(Icon::new(IconName::Plus))
|
||||||
.child(Label::new("Add a Contact"))
|
.child(Label::new("Add a Contact"))
|
||||||
.selected(is_selected)
|
.selected(is_selected)
|
||||||
.on_click(cx.listener(|this, _, cx| this.toggle_contact_finder(cx)))
|
.on_click(cx.listener(|this, _, cx| this.toggle_contact_finder(cx)))
|
||||||
|
@ -2228,47 +2256,6 @@ impl CollabPanel {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
let button_container = |cx: &mut ViewContext<Self>| {
|
|
||||||
h_stack()
|
|
||||||
.absolute()
|
|
||||||
// We're using a negative coordinate for the right anchor to
|
|
||||||
// counteract the padding of the `ListItem`.
|
|
||||||
//
|
|
||||||
// This prevents a gap from showing up between the background
|
|
||||||
// of this element and the edge of the collab panel.
|
|
||||||
.right(rems(-0.5))
|
|
||||||
// HACK: Without this the channel name clips on top of the icons, but I'm not sure why.
|
|
||||||
.z_index(10)
|
|
||||||
.bg(cx.theme().colors().panel_background)
|
|
||||||
.when(is_selected || is_active, |this| {
|
|
||||||
this.bg(cx.theme().colors().ghost_element_selected)
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
let messages_button = |cx: &mut ViewContext<Self>| {
|
|
||||||
IconButton::new("channel_chat", Icon::MessageBubbles)
|
|
||||||
.icon_size(IconSize::Small)
|
|
||||||
.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))
|
|
||||||
};
|
|
||||||
|
|
||||||
let notes_button = |cx: &mut ViewContext<Self>| {
|
|
||||||
IconButton::new("channel_notes", Icon::File)
|
|
||||||
.icon_size(IconSize::Small)
|
|
||||||
.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))
|
|
||||||
};
|
|
||||||
|
|
||||||
let width = self.width.unwrap_or(px(240.));
|
let width = self.width.unwrap_or(px(240.));
|
||||||
|
|
||||||
div()
|
div()
|
||||||
|
@ -2315,65 +2302,69 @@ impl CollabPanel {
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
.start_slot(
|
.start_slot(
|
||||||
IconElement::new(if is_public { Icon::Public } else { Icon::Hash })
|
Icon::new(if is_public {
|
||||||
.size(IconSize::Small)
|
IconName::Public
|
||||||
.color(Color::Muted),
|
} else {
|
||||||
|
IconName::Hash
|
||||||
|
})
|
||||||
|
.size(IconSize::Small)
|
||||||
|
.color(Color::Muted),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
h_stack()
|
h_stack()
|
||||||
.id(channel_id as usize)
|
.id(channel_id as usize)
|
||||||
// HACK: This is a dirty hack to help with the positioning of the button container.
|
|
||||||
//
|
|
||||||
// We're using a pixel width for the elements but then allowing the contents to
|
|
||||||
// overflow. This means that the label and facepile will be shown, but will not
|
|
||||||
// push the button container off the edge of the panel.
|
|
||||||
.w_px()
|
|
||||||
.child(Label::new(channel.name.clone()))
|
.child(Label::new(channel.name.clone()))
|
||||||
.children(face_pile.map(|face_pile| face_pile.render(cx))),
|
.children(face_pile.map(|face_pile| face_pile.render(cx))),
|
||||||
)
|
),
|
||||||
.end_slot::<Div>(
|
)
|
||||||
// If we have a notification for either button, we want to show the corresponding
|
.child(
|
||||||
// button(s) as indicators.
|
h_stack()
|
||||||
if has_messages_notification || has_notes_notification {
|
.absolute()
|
||||||
Some(
|
.right(rems(0.))
|
||||||
button_container(cx).child(
|
.h_full()
|
||||||
h_stack()
|
// HACK: Without this the channel name clips on top of the icons, but I'm not sure why.
|
||||||
.px_1()
|
.z_index(10)
|
||||||
.children(
|
.child(
|
||||||
// We only want to render the messages button if there are unseen messages.
|
h_stack()
|
||||||
// This way we don't take up any space that might overlap the channel name
|
.h_full()
|
||||||
// when there are no notifications.
|
.gap_1()
|
||||||
has_messages_notification.then(|| messages_button(cx)),
|
.px_1()
|
||||||
)
|
.child(
|
||||||
.child(
|
IconButton::new("channel_chat", IconName::MessageBubbles)
|
||||||
// We always want the notes button to take up space to prevent layout
|
.style(ButtonStyle::Filled)
|
||||||
// shift when hovering over the channel.
|
.size(ButtonSize::Compact)
|
||||||
// However, if there are is no notes notification we just show an empty slot.
|
.icon_size(IconSize::Small)
|
||||||
notes_button(cx)
|
.icon_color(if has_messages_notification {
|
||||||
.when(!has_notes_notification, |this| {
|
Color::Default
|
||||||
this.visible_on_hover("")
|
} 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))
|
||||||
|
.when(!has_messages_notification, |this| {
|
||||||
|
this.visible_on_hover("")
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
} else {
|
.child(
|
||||||
None
|
IconButton::new("channel_notes", IconName::File)
|
||||||
},
|
.style(ButtonStyle::Filled)
|
||||||
)
|
.size(ButtonSize::Compact)
|
||||||
.end_hover_slot(
|
.icon_size(IconSize::Small)
|
||||||
// When we hover the channel entry we want to always show both buttons.
|
.icon_color(if has_notes_notification {
|
||||||
button_container(cx).child(
|
Color::Default
|
||||||
h_stack()
|
} else {
|
||||||
.px_1()
|
Color::Muted
|
||||||
// The element hover background has a slight transparency to it, so we
|
})
|
||||||
// need to apply it to the inner element so that it blends with the solid
|
.on_click(cx.listener(move |this, _, cx| {
|
||||||
// background color of the absolutely-positioned element.
|
this.open_channel_notes(channel_id, cx)
|
||||||
.group_hover("", |style| {
|
}))
|
||||||
style.bg(cx.theme().colors().ghost_element_hover)
|
.tooltip(|cx| Tooltip::text("Open channel notes", cx))
|
||||||
})
|
.when(!has_notes_notification, |this| {
|
||||||
.child(messages_button(cx))
|
this.visible_on_hover("")
|
||||||
.child(notes_button(cx)),
|
}),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.tooltip(|cx| Tooltip::text("Join channel", cx))
|
.tooltip(|cx| Tooltip::text("Join channel", cx))
|
||||||
|
@ -2386,7 +2377,7 @@ impl CollabPanel {
|
||||||
.indent_level(depth + 1)
|
.indent_level(depth + 1)
|
||||||
.indent_step_size(px(20.))
|
.indent_step_size(px(20.))
|
||||||
.start_slot(
|
.start_slot(
|
||||||
IconElement::new(Icon::Hash)
|
Icon::new(IconName::Hash)
|
||||||
.size(IconSize::Small)
|
.size(IconSize::Small)
|
||||||
.color(Color::Muted),
|
.color(Color::Muted),
|
||||||
);
|
);
|
||||||
|
@ -2500,10 +2491,10 @@ impl Panel for CollabPanel {
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn icon(&self, cx: &gpui::WindowContext) -> Option<ui::Icon> {
|
fn icon(&self, cx: &gpui::WindowContext) -> Option<ui::IconName> {
|
||||||
CollaborationPanelSettings::get_global(cx)
|
CollaborationPanelSettings::get_global(cx)
|
||||||
.button
|
.button
|
||||||
.then(|| ui::Icon::Collab)
|
.then(|| ui::IconName::Collab)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
|
fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
|
||||||
|
@ -2621,11 +2612,6 @@ impl PartialEq for ListEntry {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ListEntry::GuestCount { .. } => {
|
|
||||||
if let ListEntry::GuestCount { .. } = other {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
@ -2646,11 +2632,11 @@ impl Render for DraggedChannelView {
|
||||||
.p_1()
|
.p_1()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.child(
|
.child(
|
||||||
IconElement::new(
|
Icon::new(
|
||||||
if self.channel.visibility == proto::ChannelVisibility::Public {
|
if self.channel.visibility == proto::ChannelVisibility::Public {
|
||||||
Icon::Public
|
IconName::Public
|
||||||
} else {
|
} else {
|
||||||
Icon::Hash
|
IconName::Hash
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.size(IconSize::Small)
|
.size(IconSize::Small)
|
||||||
|
|
|
@ -168,7 +168,7 @@ impl Render for ChannelModal {
|
||||||
.w_px()
|
.w_px()
|
||||||
.flex_1()
|
.flex_1()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.child(IconElement::new(Icon::Hash).size(IconSize::Medium))
|
.child(Icon::new(IconName::Hash).size(IconSize::Medium))
|
||||||
.child(Label::new(channel_name)),
|
.child(Label::new(channel_name)),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
|
@ -406,7 +406,7 @@ impl PickerDelegate for ChannelModalDelegate {
|
||||||
Some(ChannelRole::Guest) => Some(Label::new("Guest")),
|
Some(ChannelRole::Guest) => Some(Label::new("Guest")),
|
||||||
_ => None,
|
_ => None,
|
||||||
})
|
})
|
||||||
.child(IconButton::new("ellipsis", Icon::Ellipsis))
|
.child(IconButton::new("ellipsis", IconName::Ellipsis))
|
||||||
.children(
|
.children(
|
||||||
if let (Some((menu, _)), true) = (&self.context_menu, selected) {
|
if let (Some((menu, _)), true) = (&self.context_menu, selected) {
|
||||||
Some(
|
Some(
|
||||||
|
|
|
@ -155,9 +155,7 @@ impl PickerDelegate for ContactFinderDelegate {
|
||||||
.selected(selected)
|
.selected(selected)
|
||||||
.start_slot(Avatar::new(user.avatar_uri.clone()))
|
.start_slot(Avatar::new(user.avatar_uri.clone()))
|
||||||
.child(Label::new(user.github_login.clone()))
|
.child(Label::new(user.github_login.clone()))
|
||||||
.end_slot::<IconElement>(
|
.end_slot::<Icon>(icon_path.map(|icon_path| Icon::from_path(icon_path))),
|
||||||
icon_path.map(|icon_path| IconElement::from_path(icon_path)),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ 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, TintColor, Tooltip,
|
IconButton, IconName, TintColor, Tooltip,
|
||||||
};
|
};
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
use vcs_menu::{build_branch_list, BranchList, OpenRecent as ToggleVcsMenu};
|
use vcs_menu::{build_branch_list, BranchList, OpenRecent as ToggleVcsMenu};
|
||||||
|
@ -41,12 +41,6 @@ pub fn init(cx: &mut AppContext) {
|
||||||
workspace.set_titlebar_item(titlebar_item.into(), cx)
|
workspace.set_titlebar_item(titlebar_item.into(), cx)
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
// todo!()
|
|
||||||
// cx.add_action(CollabTitlebarItem::share_project);
|
|
||||||
// cx.add_action(CollabTitlebarItem::unshare_project);
|
|
||||||
// cx.add_action(CollabTitlebarItem::toggle_user_menu);
|
|
||||||
// cx.add_action(CollabTitlebarItem::toggle_vcs_menu);
|
|
||||||
// cx.add_action(CollabTitlebarItem::toggle_project_menu);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct CollabTitlebarItem {
|
pub struct CollabTitlebarItem {
|
||||||
|
@ -213,7 +207,7 @@ impl Render for CollabTitlebarItem {
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.child(
|
.child(
|
||||||
IconButton::new("leave-call", ui::Icon::Exit)
|
IconButton::new("leave-call", ui::IconName::Exit)
|
||||||
.style(ButtonStyle::Subtle)
|
.style(ButtonStyle::Subtle)
|
||||||
.tooltip(|cx| Tooltip::text("Leave call", cx))
|
.tooltip(|cx| Tooltip::text("Leave call", cx))
|
||||||
.icon_size(IconSize::Small)
|
.icon_size(IconSize::Small)
|
||||||
|
@ -230,9 +224,9 @@ impl Render for CollabTitlebarItem {
|
||||||
IconButton::new(
|
IconButton::new(
|
||||||
"mute-microphone",
|
"mute-microphone",
|
||||||
if is_muted {
|
if is_muted {
|
||||||
ui::Icon::MicMute
|
ui::IconName::MicMute
|
||||||
} else {
|
} else {
|
||||||
ui::Icon::Mic
|
ui::IconName::Mic
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.tooltip(move |cx| {
|
.tooltip(move |cx| {
|
||||||
|
@ -256,9 +250,9 @@ impl Render for CollabTitlebarItem {
|
||||||
IconButton::new(
|
IconButton::new(
|
||||||
"mute-sound",
|
"mute-sound",
|
||||||
if is_deafened {
|
if is_deafened {
|
||||||
ui::Icon::AudioOff
|
ui::IconName::AudioOff
|
||||||
} else {
|
} else {
|
||||||
ui::Icon::AudioOn
|
ui::IconName::AudioOn
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.style(ButtonStyle::Subtle)
|
.style(ButtonStyle::Subtle)
|
||||||
|
@ -281,7 +275,7 @@ impl Render for CollabTitlebarItem {
|
||||||
)
|
)
|
||||||
.when(!read_only, |this| {
|
.when(!read_only, |this| {
|
||||||
this.child(
|
this.child(
|
||||||
IconButton::new("screen-share", ui::Icon::Screen)
|
IconButton::new("screen-share", ui::IconName::Screen)
|
||||||
.style(ButtonStyle::Subtle)
|
.style(ButtonStyle::Subtle)
|
||||||
.icon_size(IconSize::Small)
|
.icon_size(IconSize::Small)
|
||||||
.selected(is_screen_sharing)
|
.selected(is_screen_sharing)
|
||||||
|
@ -573,7 +567,7 @@ impl CollabTitlebarItem {
|
||||||
| client::Status::ReconnectionError { .. } => Some(
|
| client::Status::ReconnectionError { .. } => Some(
|
||||||
div()
|
div()
|
||||||
.id("disconnected")
|
.id("disconnected")
|
||||||
.child(IconElement::new(Icon::Disconnected).size(IconSize::Small))
|
.child(Icon::new(IconName::Disconnected).size(IconSize::Small))
|
||||||
.tooltip(|cx| Tooltip::text("Disconnected", cx))
|
.tooltip(|cx| Tooltip::text("Disconnected", cx))
|
||||||
.into_any_element(),
|
.into_any_element(),
|
||||||
),
|
),
|
||||||
|
@ -643,7 +637,7 @@ impl CollabTitlebarItem {
|
||||||
h_stack()
|
h_stack()
|
||||||
.gap_0p5()
|
.gap_0p5()
|
||||||
.child(Avatar::new(user.avatar_uri.clone()))
|
.child(Avatar::new(user.avatar_uri.clone()))
|
||||||
.child(IconElement::new(Icon::ChevronDown).color(Color::Muted)),
|
.child(Icon::new(IconName::ChevronDown).color(Color::Muted)),
|
||||||
)
|
)
|
||||||
.style(ButtonStyle::Subtle)
|
.style(ButtonStyle::Subtle)
|
||||||
.tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)),
|
.tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)),
|
||||||
|
@ -665,7 +659,7 @@ impl CollabTitlebarItem {
|
||||||
.child(
|
.child(
|
||||||
h_stack()
|
h_stack()
|
||||||
.gap_0p5()
|
.gap_0p5()
|
||||||
.child(IconElement::new(Icon::ChevronDown).color(Color::Muted)),
|
.child(Icon::new(IconName::ChevronDown).color(Color::Muted)),
|
||||||
)
|
)
|
||||||
.style(ButtonStyle::Subtle)
|
.style(ButtonStyle::Subtle)
|
||||||
.tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)),
|
.tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)),
|
||||||
|
|
|
@ -19,7 +19,7 @@ use serde::{Deserialize, Serialize};
|
||||||
use settings::{Settings, SettingsStore};
|
use settings::{Settings, SettingsStore};
|
||||||
use std::{sync::Arc, time::Duration};
|
use std::{sync::Arc, time::Duration};
|
||||||
use time::{OffsetDateTime, UtcOffset};
|
use time::{OffsetDateTime, UtcOffset};
|
||||||
use ui::{h_stack, prelude::*, v_stack, Avatar, Button, Icon, IconButton, IconElement, Label};
|
use ui::{h_stack, prelude::*, v_stack, Avatar, Button, Icon, IconButton, IconName, Label};
|
||||||
use util::{ResultExt, TryFutureExt};
|
use util::{ResultExt, TryFutureExt};
|
||||||
use workspace::{
|
use workspace::{
|
||||||
dock::{DockPosition, Panel, PanelEvent},
|
dock::{DockPosition, Panel, PanelEvent},
|
||||||
|
@ -553,7 +553,7 @@ impl Render for NotificationPanel {
|
||||||
.border_b_1()
|
.border_b_1()
|
||||||
.border_color(cx.theme().colors().border)
|
.border_color(cx.theme().colors().border)
|
||||||
.child(Label::new("Notifications"))
|
.child(Label::new("Notifications"))
|
||||||
.child(IconElement::new(Icon::Envelope)),
|
.child(Icon::new(IconName::Envelope)),
|
||||||
)
|
)
|
||||||
.map(|this| {
|
.map(|this| {
|
||||||
if self.client.user_id().is_none() {
|
if self.client.user_id().is_none() {
|
||||||
|
@ -564,7 +564,7 @@ impl Render for NotificationPanel {
|
||||||
.child(
|
.child(
|
||||||
Button::new("sign_in_prompt_button", "Sign in")
|
Button::new("sign_in_prompt_button", "Sign in")
|
||||||
.icon_color(Color::Muted)
|
.icon_color(Color::Muted)
|
||||||
.icon(Icon::Github)
|
.icon(IconName::Github)
|
||||||
.icon_position(IconPosition::Start)
|
.icon_position(IconPosition::Start)
|
||||||
.style(ButtonStyle::Filled)
|
.style(ButtonStyle::Filled)
|
||||||
.full_width()
|
.full_width()
|
||||||
|
@ -655,10 +655,10 @@ impl Panel for NotificationPanel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn icon(&self, cx: &gpui::WindowContext) -> Option<Icon> {
|
fn icon(&self, cx: &gpui::WindowContext) -> Option<IconName> {
|
||||||
(NotificationPanelSettings::get_global(cx).button
|
(NotificationPanelSettings::get_global(cx).button
|
||||||
&& self.notification_store.read(cx).notification_count() > 0)
|
&& self.notification_store.read(cx).notification_count() > 0)
|
||||||
.then(|| Icon::Bell)
|
.then(|| IconName::Bell)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
|
fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
|
||||||
|
@ -716,7 +716,7 @@ impl Render for NotificationToast {
|
||||||
.children(user.map(|user| Avatar::new(user.avatar_uri.clone())))
|
.children(user.map(|user| Avatar::new(user.avatar_uri.clone())))
|
||||||
.child(Label::new(self.text.clone()))
|
.child(Label::new(self.text.clone()))
|
||||||
.child(
|
.child(
|
||||||
IconButton::new("close", Icon::Close)
|
IconButton::new("close", IconName::Close)
|
||||||
.on_click(cx.listener(|_, _, cx| cx.emit(DismissEvent))),
|
.on_click(cx.listener(|_, _, cx| cx.emit(DismissEvent))),
|
||||||
)
|
)
|
||||||
.on_click(cx.listener(|this, _, cx| {
|
.on_click(cx.listener(|this, _, cx| {
|
||||||
|
|
|
@ -1,9 +1,16 @@
|
||||||
|
mod collab_notification;
|
||||||
|
pub mod incoming_call_notification;
|
||||||
|
pub mod project_shared_notification;
|
||||||
|
|
||||||
|
#[cfg(feature = "stories")]
|
||||||
|
mod stories;
|
||||||
|
|
||||||
use gpui::AppContext;
|
use gpui::AppContext;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use workspace::AppState;
|
use workspace::AppState;
|
||||||
|
|
||||||
pub mod incoming_call_notification;
|
#[cfg(feature = "stories")]
|
||||||
pub mod project_shared_notification;
|
pub use stories::*;
|
||||||
|
|
||||||
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||||
incoming_call_notification::init(app_state, cx);
|
incoming_call_notification::init(app_state, cx);
|
||||||
|
|
52
crates/collab_ui/src/notifications/collab_notification.rs
Normal file
52
crates/collab_ui/src/notifications/collab_notification.rs
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
use gpui::{img, prelude::*, AnyElement, SharedUrl};
|
||||||
|
use smallvec::SmallVec;
|
||||||
|
use ui::prelude::*;
|
||||||
|
|
||||||
|
#[derive(IntoElement)]
|
||||||
|
pub struct CollabNotification {
|
||||||
|
avatar_uri: SharedUrl,
|
||||||
|
accept_button: Button,
|
||||||
|
dismiss_button: Button,
|
||||||
|
children: SmallVec<[AnyElement; 2]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CollabNotification {
|
||||||
|
pub fn new(
|
||||||
|
avatar_uri: impl Into<SharedUrl>,
|
||||||
|
accept_button: Button,
|
||||||
|
dismiss_button: Button,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
avatar_uri: avatar_uri.into(),
|
||||||
|
accept_button,
|
||||||
|
dismiss_button,
|
||||||
|
children: SmallVec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ParentElement for CollabNotification {
|
||||||
|
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
|
||||||
|
&mut self.children
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderOnce for CollabNotification {
|
||||||
|
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||||
|
h_stack()
|
||||||
|
.text_ui()
|
||||||
|
.justify_between()
|
||||||
|
.size_full()
|
||||||
|
.overflow_hidden()
|
||||||
|
.elevation_3(cx)
|
||||||
|
.p_2()
|
||||||
|
.gap_2()
|
||||||
|
.child(img(self.avatar_uri).w_12().h_12().rounded_full())
|
||||||
|
.child(v_stack().overflow_hidden().children(self.children))
|
||||||
|
.child(
|
||||||
|
v_stack()
|
||||||
|
.child(self.accept_button)
|
||||||
|
.child(self.dismiss_button),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,15 +1,12 @@
|
||||||
use crate::notification_window_options;
|
use crate::notification_window_options;
|
||||||
|
use crate::notifications::collab_notification::CollabNotification;
|
||||||
use call::{ActiveCall, IncomingCall};
|
use call::{ActiveCall, IncomingCall};
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use gpui::{
|
use gpui::{prelude::*, AppContext, WindowHandle};
|
||||||
img, px, AppContext, ParentElement, Render, RenderOnce, Styled, ViewContext,
|
|
||||||
VisualContext as _, WindowHandle,
|
|
||||||
};
|
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use std::sync::{Arc, Weak};
|
use std::sync::{Arc, Weak};
|
||||||
use theme::ThemeSettings;
|
use theme::ThemeSettings;
|
||||||
use ui::prelude::*;
|
use ui::{prelude::*, Button, Label};
|
||||||
use ui::{h_stack, v_stack, Button, Label};
|
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
use workspace::AppState;
|
use workspace::AppState;
|
||||||
|
|
||||||
|
@ -22,7 +19,6 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||||
for window in notification_windows.drain(..) {
|
for window in notification_windows.drain(..) {
|
||||||
window
|
window
|
||||||
.update(&mut cx, |_, cx| {
|
.update(&mut cx, |_, cx| {
|
||||||
// todo!()
|
|
||||||
cx.remove_window();
|
cx.remove_window();
|
||||||
})
|
})
|
||||||
.log_err();
|
.log_err();
|
||||||
|
@ -31,8 +27,8 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||||
if let Some(incoming_call) = incoming_call {
|
if let Some(incoming_call) = incoming_call {
|
||||||
let unique_screens = cx.update(|cx| cx.displays()).unwrap();
|
let unique_screens = cx.update(|cx| cx.displays()).unwrap();
|
||||||
let window_size = gpui::Size {
|
let window_size = gpui::Size {
|
||||||
width: px(380.),
|
width: px(400.),
|
||||||
height: px(64.),
|
height: px(72.),
|
||||||
};
|
};
|
||||||
|
|
||||||
for screen in unique_screens {
|
for screen in unique_screens {
|
||||||
|
@ -129,35 +125,22 @@ impl Render for IncomingCallNotification {
|
||||||
|
|
||||||
cx.set_rem_size(ui_font_size);
|
cx.set_rem_size(ui_font_size);
|
||||||
|
|
||||||
h_stack()
|
div().size_full().font(ui_font).child(
|
||||||
.font(ui_font)
|
CollabNotification::new(
|
||||||
.text_ui()
|
self.state.call.calling_user.avatar_uri.clone(),
|
||||||
.justify_between()
|
Button::new("accept", "Accept").on_click({
|
||||||
.size_full()
|
let state = self.state.clone();
|
||||||
.overflow_hidden()
|
move |_, cx| state.respond(true, cx)
|
||||||
.elevation_3(cx)
|
}),
|
||||||
.p_2()
|
Button::new("decline", "Decline").on_click({
|
||||||
.gap_2()
|
let state = self.state.clone();
|
||||||
.child(
|
move |_, cx| state.respond(false, cx)
|
||||||
img(self.state.call.calling_user.avatar_uri.clone())
|
}),
|
||||||
.w_12()
|
|
||||||
.h_12()
|
|
||||||
.rounded_full(),
|
|
||||||
)
|
)
|
||||||
.child(v_stack().overflow_hidden().child(Label::new(format!(
|
.child(v_stack().overflow_hidden().child(Label::new(format!(
|
||||||
"{} is sharing a project in Zed",
|
"{} is sharing a project in Zed",
|
||||||
self.state.call.calling_user.github_login
|
self.state.call.calling_user.github_login
|
||||||
))))
|
)))),
|
||||||
.child(
|
)
|
||||||
v_stack()
|
|
||||||
.child(Button::new("accept", "Accept").render(cx).on_click({
|
|
||||||
let state = self.state.clone();
|
|
||||||
move |_, cx| state.respond(true, cx)
|
|
||||||
}))
|
|
||||||
.child(Button::new("decline", "Decline").render(cx).on_click({
|
|
||||||
let state = self.state.clone();
|
|
||||||
move |_, cx| state.respond(false, cx)
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
use crate::notification_window_options;
|
use crate::notification_window_options;
|
||||||
|
use crate::notifications::collab_notification::CollabNotification;
|
||||||
use call::{room, ActiveCall};
|
use call::{room, ActiveCall};
|
||||||
use client::User;
|
use client::User;
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use gpui::{img, px, AppContext, ParentElement, Render, Size, Styled, ViewContext, VisualContext};
|
use gpui::{AppContext, Size};
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use std::sync::{Arc, Weak};
|
use std::sync::{Arc, Weak};
|
||||||
use theme::ThemeSettings;
|
use theme::ThemeSettings;
|
||||||
use ui::{h_stack, prelude::*, v_stack, Button, Label};
|
use ui::{prelude::*, Button, Label};
|
||||||
use workspace::AppState;
|
use workspace::AppState;
|
||||||
|
|
||||||
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||||
|
@ -50,7 +51,6 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||||
for window in windows {
|
for window in windows {
|
||||||
window
|
window
|
||||||
.update(cx, |_, cx| {
|
.update(cx, |_, cx| {
|
||||||
// todo!()
|
|
||||||
cx.remove_window();
|
cx.remove_window();
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
|
@ -63,7 +63,6 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||||
for window in windows {
|
for window in windows {
|
||||||
window
|
window
|
||||||
.update(cx, |_, cx| {
|
.update(cx, |_, cx| {
|
||||||
// todo!()
|
|
||||||
cx.remove_window();
|
cx.remove_window();
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
|
@ -130,51 +129,30 @@ impl Render for ProjectSharedNotification {
|
||||||
|
|
||||||
cx.set_rem_size(ui_font_size);
|
cx.set_rem_size(ui_font_size);
|
||||||
|
|
||||||
h_stack()
|
div().size_full().font(ui_font).child(
|
||||||
.font(ui_font)
|
CollabNotification::new(
|
||||||
.text_ui()
|
self.owner.avatar_uri.clone(),
|
||||||
.justify_between()
|
Button::new("open", "Open").on_click(cx.listener(move |this, _event, cx| {
|
||||||
.size_full()
|
this.join(cx);
|
||||||
.overflow_hidden()
|
})),
|
||||||
.elevation_3(cx)
|
Button::new("dismiss", "Dismiss").on_click(cx.listener(move |this, _event, cx| {
|
||||||
.p_2()
|
this.dismiss(cx);
|
||||||
.gap_2()
|
})),
|
||||||
.child(
|
|
||||||
img(self.owner.avatar_uri.clone())
|
|
||||||
.w_12()
|
|
||||||
.h_12()
|
|
||||||
.rounded_full(),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
v_stack()
|
|
||||||
.overflow_hidden()
|
|
||||||
.child(Label::new(self.owner.github_login.clone()))
|
|
||||||
.child(Label::new(format!(
|
|
||||||
"is sharing a project in Zed{}",
|
|
||||||
if self.worktree_root_names.is_empty() {
|
|
||||||
""
|
|
||||||
} else {
|
|
||||||
":"
|
|
||||||
}
|
|
||||||
)))
|
|
||||||
.children(if self.worktree_root_names.is_empty() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(Label::new(self.worktree_root_names.join(", ")))
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
v_stack()
|
|
||||||
.child(Button::new("open", "Open").on_click(cx.listener(
|
|
||||||
move |this, _event, cx| {
|
|
||||||
this.join(cx);
|
|
||||||
},
|
|
||||||
)))
|
|
||||||
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
|
|
||||||
move |this, _event, cx| {
|
|
||||||
this.dismiss(cx);
|
|
||||||
},
|
|
||||||
))),
|
|
||||||
)
|
)
|
||||||
|
.child(Label::new(self.owner.github_login.clone()))
|
||||||
|
.child(Label::new(format!(
|
||||||
|
"is sharing a project in Zed{}",
|
||||||
|
if self.worktree_root_names.is_empty() {
|
||||||
|
""
|
||||||
|
} else {
|
||||||
|
":"
|
||||||
|
}
|
||||||
|
)))
|
||||||
|
.children(if self.worktree_root_names.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(Label::new(self.worktree_root_names.join(", ")))
|
||||||
|
}),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
3
crates/collab_ui/src/notifications/stories.rs
Normal file
3
crates/collab_ui/src/notifications/stories.rs
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
mod collab_notification;
|
||||||
|
|
||||||
|
pub use collab_notification::*;
|
|
@ -0,0 +1,50 @@
|
||||||
|
use gpui::prelude::*;
|
||||||
|
use story::{StoryContainer, StoryItem, StorySection};
|
||||||
|
use ui::prelude::*;
|
||||||
|
|
||||||
|
use crate::notifications::collab_notification::CollabNotification;
|
||||||
|
|
||||||
|
pub struct CollabNotificationStory;
|
||||||
|
|
||||||
|
impl Render for CollabNotificationStory {
|
||||||
|
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
|
let window_container = |width, height| div().w(px(width)).h(px(height));
|
||||||
|
|
||||||
|
StoryContainer::new(
|
||||||
|
"CollabNotification Story",
|
||||||
|
"crates/collab_ui/src/notifications/stories/collab_notification.rs",
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
StorySection::new().child(StoryItem::new(
|
||||||
|
"Incoming Call Notification",
|
||||||
|
window_container(400., 72.).child(
|
||||||
|
CollabNotification::new(
|
||||||
|
"https://avatars.githubusercontent.com/u/1486634?v=4",
|
||||||
|
Button::new("accept", "Accept"),
|
||||||
|
Button::new("decline", "Decline"),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
v_stack()
|
||||||
|
.overflow_hidden()
|
||||||
|
.child(Label::new("maxdeviant is sharing a project in Zed")),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
StorySection::new().child(StoryItem::new(
|
||||||
|
"Project Shared Notification",
|
||||||
|
window_container(400., 72.).child(
|
||||||
|
CollabNotification::new(
|
||||||
|
"https://avatars.githubusercontent.com/u/1714999?v=4",
|
||||||
|
Button::new("open", "Open"),
|
||||||
|
Button::new("dismiss", "Dismiss"),
|
||||||
|
)
|
||||||
|
.child(Label::new("iamnbutler"))
|
||||||
|
.child(Label::new("is sharing a project in Zed:"))
|
||||||
|
.child(Label::new("zed")),
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,7 +17,9 @@ use util::{paths, ResultExt};
|
||||||
use workspace::{
|
use workspace::{
|
||||||
create_and_open_local_file,
|
create_and_open_local_file,
|
||||||
item::ItemHandle,
|
item::ItemHandle,
|
||||||
ui::{popover_menu, ButtonCommon, Clickable, ContextMenu, Icon, IconButton, IconSize, Tooltip},
|
ui::{
|
||||||
|
popover_menu, ButtonCommon, Clickable, ContextMenu, IconButton, IconName, IconSize, Tooltip,
|
||||||
|
},
|
||||||
StatusItemView, Toast, Workspace,
|
StatusItemView, Toast, Workspace,
|
||||||
};
|
};
|
||||||
use zed_actions::OpenBrowser;
|
use zed_actions::OpenBrowser;
|
||||||
|
@ -51,15 +53,15 @@ impl Render for CopilotButton {
|
||||||
.unwrap_or_else(|| all_language_settings.copilot_enabled(None, None));
|
.unwrap_or_else(|| all_language_settings.copilot_enabled(None, None));
|
||||||
|
|
||||||
let icon = match status {
|
let icon = match status {
|
||||||
Status::Error(_) => Icon::CopilotError,
|
Status::Error(_) => IconName::CopilotError,
|
||||||
Status::Authorized => {
|
Status::Authorized => {
|
||||||
if enabled {
|
if enabled {
|
||||||
Icon::Copilot
|
IconName::Copilot
|
||||||
} else {
|
} else {
|
||||||
Icon::CopilotDisabled
|
IconName::CopilotDisabled
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => Icon::CopilotInit,
|
_ => IconName::CopilotInit,
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Status::Error(e) = status {
|
if let Status::Error(e) = status {
|
||||||
|
|
|
@ -4,7 +4,7 @@ use gpui::{
|
||||||
FocusableView, InteractiveElement, IntoElement, Model, ParentElement, Render, Styled,
|
FocusableView, InteractiveElement, IntoElement, Model, ParentElement, Render, Styled,
|
||||||
Subscription, ViewContext,
|
Subscription, ViewContext,
|
||||||
};
|
};
|
||||||
use ui::{prelude::*, Button, Icon, Label};
|
use ui::{prelude::*, Button, IconName, Label};
|
||||||
use workspace::ModalView;
|
use workspace::ModalView;
|
||||||
|
|
||||||
const COPILOT_SIGN_UP_URL: &'static str = "https://github.com/features/copilot";
|
const COPILOT_SIGN_UP_URL: &'static str = "https://github.com/features/copilot";
|
||||||
|
@ -175,7 +175,7 @@ impl Render for CopilotCodeVerification {
|
||||||
.w_32()
|
.w_32()
|
||||||
.h_16()
|
.h_16()
|
||||||
.flex_none()
|
.flex_none()
|
||||||
.path(Icon::ZedXCopilot.path())
|
.path(IconName::ZedXCopilot.path())
|
||||||
.text_color(cx.theme().colors().icon),
|
.text_color(cx.theme().colors().icon),
|
||||||
)
|
)
|
||||||
.child(prompt)
|
.child(prompt)
|
||||||
|
|
|
@ -36,7 +36,7 @@ use std::{
|
||||||
};
|
};
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
pub use toolbar_controls::ToolbarControls;
|
pub use toolbar_controls::ToolbarControls;
|
||||||
use ui::{h_stack, prelude::*, Icon, IconElement, Label};
|
use ui::{h_stack, prelude::*, Icon, IconName, Label};
|
||||||
use util::TryFutureExt;
|
use util::TryFutureExt;
|
||||||
use workspace::{
|
use workspace::{
|
||||||
item::{BreadcrumbText, Item, ItemEvent, ItemHandle},
|
item::{BreadcrumbText, Item, ItemEvent, ItemHandle},
|
||||||
|
@ -660,7 +660,7 @@ impl Item for ProjectDiagnosticsEditor {
|
||||||
then.child(
|
then.child(
|
||||||
h_stack()
|
h_stack()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.child(IconElement::new(Icon::XCircle).color(Color::Error))
|
.child(Icon::new(IconName::XCircle).color(Color::Error))
|
||||||
.child(Label::new(self.summary.error_count.to_string()).color(
|
.child(Label::new(self.summary.error_count.to_string()).color(
|
||||||
if selected {
|
if selected {
|
||||||
Color::Default
|
Color::Default
|
||||||
|
@ -674,9 +674,7 @@ impl Item for ProjectDiagnosticsEditor {
|
||||||
then.child(
|
then.child(
|
||||||
h_stack()
|
h_stack()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.child(
|
.child(Icon::new(IconName::ExclamationTriangle).color(Color::Warning))
|
||||||
IconElement::new(Icon::ExclamationTriangle).color(Color::Warning),
|
|
||||||
)
|
|
||||||
.child(Label::new(self.summary.warning_count.to_string()).color(
|
.child(Label::new(self.summary.warning_count.to_string()).color(
|
||||||
if selected {
|
if selected {
|
||||||
Color::Default
|
Color::Default
|
||||||
|
@ -816,10 +814,10 @@ fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
|
||||||
.flex_none()
|
.flex_none()
|
||||||
.map(|icon| {
|
.map(|icon| {
|
||||||
if diagnostic.severity == DiagnosticSeverity::ERROR {
|
if diagnostic.severity == DiagnosticSeverity::ERROR {
|
||||||
icon.path(Icon::XCircle.path())
|
icon.path(IconName::XCircle.path())
|
||||||
.text_color(Color::Error.color(cx))
|
.text_color(Color::Error.color(cx))
|
||||||
} else {
|
} else {
|
||||||
icon.path(Icon::ExclamationTriangle.path())
|
icon.path(IconName::ExclamationTriangle.path())
|
||||||
.text_color(Color::Warning.color(cx))
|
.text_color(Color::Warning.color(cx))
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -6,7 +6,7 @@ use gpui::{
|
||||||
};
|
};
|
||||||
use language::Diagnostic;
|
use language::Diagnostic;
|
||||||
use lsp::LanguageServerId;
|
use lsp::LanguageServerId;
|
||||||
use ui::{h_stack, prelude::*, Button, ButtonLike, Color, Icon, IconElement, Label, Tooltip};
|
use ui::{h_stack, prelude::*, Button, ButtonLike, Color, Icon, IconName, Label, Tooltip};
|
||||||
use workspace::{item::ItemHandle, StatusItemView, ToolbarItemEvent, Workspace};
|
use workspace::{item::ItemHandle, StatusItemView, ToolbarItemEvent, Workspace};
|
||||||
|
|
||||||
use crate::{Deploy, ProjectDiagnosticsEditor};
|
use crate::{Deploy, ProjectDiagnosticsEditor};
|
||||||
|
@ -25,7 +25,7 @@ impl Render for DiagnosticIndicator {
|
||||||
let diagnostic_indicator = match (self.summary.error_count, self.summary.warning_count) {
|
let diagnostic_indicator = match (self.summary.error_count, self.summary.warning_count) {
|
||||||
(0, 0) => h_stack().map(|this| {
|
(0, 0) => h_stack().map(|this| {
|
||||||
this.child(
|
this.child(
|
||||||
IconElement::new(Icon::Check)
|
Icon::new(IconName::Check)
|
||||||
.size(IconSize::Small)
|
.size(IconSize::Small)
|
||||||
.color(Color::Default),
|
.color(Color::Default),
|
||||||
)
|
)
|
||||||
|
@ -33,7 +33,7 @@ impl Render for DiagnosticIndicator {
|
||||||
(0, warning_count) => h_stack()
|
(0, warning_count) => h_stack()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.child(
|
.child(
|
||||||
IconElement::new(Icon::ExclamationTriangle)
|
Icon::new(IconName::ExclamationTriangle)
|
||||||
.size(IconSize::Small)
|
.size(IconSize::Small)
|
||||||
.color(Color::Warning),
|
.color(Color::Warning),
|
||||||
)
|
)
|
||||||
|
@ -41,7 +41,7 @@ impl Render for DiagnosticIndicator {
|
||||||
(error_count, 0) => h_stack()
|
(error_count, 0) => h_stack()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.child(
|
.child(
|
||||||
IconElement::new(Icon::XCircle)
|
Icon::new(IconName::XCircle)
|
||||||
.size(IconSize::Small)
|
.size(IconSize::Small)
|
||||||
.color(Color::Error),
|
.color(Color::Error),
|
||||||
)
|
)
|
||||||
|
@ -49,13 +49,13 @@ impl Render for DiagnosticIndicator {
|
||||||
(error_count, warning_count) => h_stack()
|
(error_count, warning_count) => h_stack()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.child(
|
.child(
|
||||||
IconElement::new(Icon::XCircle)
|
Icon::new(IconName::XCircle)
|
||||||
.size(IconSize::Small)
|
.size(IconSize::Small)
|
||||||
.color(Color::Error),
|
.color(Color::Error),
|
||||||
)
|
)
|
||||||
.child(Label::new(error_count.to_string()).size(LabelSize::Small))
|
.child(Label::new(error_count.to_string()).size(LabelSize::Small))
|
||||||
.child(
|
.child(
|
||||||
IconElement::new(Icon::ExclamationTriangle)
|
Icon::new(IconName::ExclamationTriangle)
|
||||||
.size(IconSize::Small)
|
.size(IconSize::Small)
|
||||||
.color(Color::Warning),
|
.color(Color::Warning),
|
||||||
)
|
)
|
||||||
|
@ -66,7 +66,7 @@ impl Render for DiagnosticIndicator {
|
||||||
Some(
|
Some(
|
||||||
h_stack()
|
h_stack()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.child(IconElement::new(Icon::ArrowCircle).size(IconSize::Small))
|
.child(Icon::new(IconName::ArrowCircle).size(IconSize::Small))
|
||||||
.child(
|
.child(
|
||||||
Label::new("Checking…")
|
Label::new("Checking…")
|
||||||
.size(LabelSize::Small)
|
.size(LabelSize::Small)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use crate::ProjectDiagnosticsEditor;
|
use crate::ProjectDiagnosticsEditor;
|
||||||
use gpui::{div, EventEmitter, ParentElement, Render, ViewContext, WeakView};
|
use gpui::{div, EventEmitter, ParentElement, Render, ViewContext, WeakView};
|
||||||
use ui::prelude::*;
|
use ui::prelude::*;
|
||||||
use ui::{Icon, IconButton, Tooltip};
|
use ui::{IconButton, IconName, Tooltip};
|
||||||
use workspace::{item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
|
use workspace::{item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
|
||||||
|
|
||||||
pub struct ToolbarControls {
|
pub struct ToolbarControls {
|
||||||
|
@ -24,7 +24,7 @@ impl Render for ToolbarControls {
|
||||||
};
|
};
|
||||||
|
|
||||||
div().child(
|
div().child(
|
||||||
IconButton::new("toggle-warnings", Icon::ExclamationTriangle)
|
IconButton::new("toggle-warnings", IconName::ExclamationTriangle)
|
||||||
.tooltip(move |cx| Tooltip::text(tooltip, cx))
|
.tooltip(move |cx| Tooltip::text(tooltip, cx))
|
||||||
.on_click(cx.listener(|this, _, cx| {
|
.on_click(cx.listener(|this, _, cx| {
|
||||||
if let Some(editor) = this.editor.as_ref().and_then(|editor| editor.upgrade()) {
|
if let Some(editor) = this.editor.as_ref().and_then(|editor| editor.upgrade()) {
|
||||||
|
|
|
@ -1015,7 +1015,6 @@ pub mod tests {
|
||||||
.map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
|
.map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
|
||||||
.unwrap_or(10);
|
.unwrap_or(10);
|
||||||
|
|
||||||
let _test_platform = &cx.test_platform;
|
|
||||||
let mut tab_size = rng.gen_range(1..=4);
|
let mut tab_size = rng.gen_range(1..=4);
|
||||||
let buffer_start_excerpt_header_height = rng.gen_range(1..=5);
|
let buffer_start_excerpt_header_height = rng.gen_range(1..=5);
|
||||||
let excerpt_header_height = rng.gen_range(1..=5);
|
let excerpt_header_height = rng.gen_range(1..=5);
|
||||||
|
|
|
@ -99,8 +99,8 @@ use sum_tree::TreeMap;
|
||||||
use text::{OffsetUtf16, Rope};
|
use text::{OffsetUtf16, Rope};
|
||||||
use theme::{ActiveTheme, PlayerColor, StatusColors, SyntaxTheme, ThemeColors, ThemeSettings};
|
use theme::{ActiveTheme, PlayerColor, StatusColors, SyntaxTheme, ThemeColors, ThemeSettings};
|
||||||
use ui::{
|
use ui::{
|
||||||
h_stack, prelude::*, ButtonSize, ButtonStyle, Icon, IconButton, IconSize, ListItem, Popover,
|
h_stack, prelude::*, ButtonSize, ButtonStyle, IconButton, IconName, IconSize, ListItem,
|
||||||
Tooltip,
|
Popover, Tooltip,
|
||||||
};
|
};
|
||||||
use util::{post_inc, RangeExt, ResultExt, TryFutureExt};
|
use util::{post_inc, RangeExt, ResultExt, TryFutureExt};
|
||||||
use workspace::{searchable::SearchEvent, ItemNavHistory, Pane, SplitDirection, ViewId, Workspace};
|
use workspace::{searchable::SearchEvent, ItemNavHistory, Pane, SplitDirection, ViewId, Workspace};
|
||||||
|
@ -507,7 +507,7 @@ pub enum SoftWrap {
|
||||||
Column(u32),
|
Column(u32),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Default)]
|
#[derive(Clone)]
|
||||||
pub struct EditorStyle {
|
pub struct EditorStyle {
|
||||||
pub background: Hsla,
|
pub background: Hsla,
|
||||||
pub local_player: PlayerColor,
|
pub local_player: PlayerColor,
|
||||||
|
@ -519,6 +519,24 @@ pub struct EditorStyle {
|
||||||
pub suggestions_style: HighlightStyle,
|
pub suggestions_style: HighlightStyle,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for EditorStyle {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
background: Hsla::default(),
|
||||||
|
local_player: PlayerColor::default(),
|
||||||
|
text: TextStyle::default(),
|
||||||
|
scrollbar_width: Pixels::default(),
|
||||||
|
syntax: Default::default(),
|
||||||
|
// HACK: Status colors don't have a real default.
|
||||||
|
// We should look into removing the status colors from the editor
|
||||||
|
// style and retrieve them directly from the theme.
|
||||||
|
status: StatusColors::dark(),
|
||||||
|
inlays_style: HighlightStyle::default(),
|
||||||
|
suggestions_style: HighlightStyle::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type CompletionId = usize;
|
type CompletionId = usize;
|
||||||
|
|
||||||
// type GetFieldEditorTheme = dyn Fn(&theme::Theme) -> theme::FieldEditor;
|
// type GetFieldEditorTheme = dyn Fn(&theme::Theme) -> theme::FieldEditor;
|
||||||
|
@ -1811,10 +1829,6 @@ impl Editor {
|
||||||
this.end_selection(cx);
|
this.end_selection(cx);
|
||||||
this.scroll_manager.show_scrollbar(cx);
|
this.scroll_manager.show_scrollbar(cx);
|
||||||
|
|
||||||
// todo!("use a different mechanism")
|
|
||||||
// let editor_created_event = EditorCreated(cx.handle());
|
|
||||||
// cx.emit_global(editor_created_event);
|
|
||||||
|
|
||||||
if mode == EditorMode::Full {
|
if mode == EditorMode::Full {
|
||||||
let should_auto_hide_scrollbars = cx.should_auto_hide_scrollbars();
|
let should_auto_hide_scrollbars = cx.should_auto_hide_scrollbars();
|
||||||
cx.set_global(ScrollbarAutoHide(should_auto_hide_scrollbars));
|
cx.set_global(ScrollbarAutoHide(should_auto_hide_scrollbars));
|
||||||
|
@ -4223,7 +4237,7 @@ impl Editor {
|
||||||
) -> Option<IconButton> {
|
) -> Option<IconButton> {
|
||||||
if self.available_code_actions.is_some() {
|
if self.available_code_actions.is_some() {
|
||||||
Some(
|
Some(
|
||||||
IconButton::new("code_actions_indicator", ui::Icon::Bolt)
|
IconButton::new("code_actions_indicator", ui::IconName::Bolt)
|
||||||
.icon_size(IconSize::Small)
|
.icon_size(IconSize::Small)
|
||||||
.icon_color(Color::Muted)
|
.icon_color(Color::Muted)
|
||||||
.selected(is_active)
|
.selected(is_active)
|
||||||
|
@ -4257,7 +4271,7 @@ impl Editor {
|
||||||
fold_data
|
fold_data
|
||||||
.map(|(fold_status, buffer_row, active)| {
|
.map(|(fold_status, buffer_row, active)| {
|
||||||
(active || gutter_hovered || fold_status == FoldStatus::Folded).then(|| {
|
(active || gutter_hovered || fold_status == FoldStatus::Folded).then(|| {
|
||||||
IconButton::new(ix as usize, ui::Icon::ChevronDown)
|
IconButton::new(ix as usize, ui::IconName::ChevronDown)
|
||||||
.on_click(cx.listener(move |editor, _e, cx| match fold_status {
|
.on_click(cx.listener(move |editor, _e, cx| match fold_status {
|
||||||
FoldStatus::Folded => {
|
FoldStatus::Folded => {
|
||||||
editor.unfold_at(&UnfoldAt { buffer_row }, cx);
|
editor.unfold_at(&UnfoldAt { buffer_row }, cx);
|
||||||
|
@ -4269,7 +4283,7 @@ impl Editor {
|
||||||
.icon_color(ui::Color::Muted)
|
.icon_color(ui::Color::Muted)
|
||||||
.icon_size(ui::IconSize::Small)
|
.icon_size(ui::IconSize::Small)
|
||||||
.selected(fold_status == FoldStatus::Folded)
|
.selected(fold_status == FoldStatus::Folded)
|
||||||
.selected_icon(ui::Icon::ChevronRight)
|
.selected_icon(ui::IconName::ChevronRight)
|
||||||
.size(ui::ButtonSize::None)
|
.size(ui::ButtonSize::None)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -7036,7 +7050,7 @@ impl Editor {
|
||||||
let buffer = self.buffer.read(cx).snapshot(cx);
|
let buffer = self.buffer.read(cx).snapshot(cx);
|
||||||
let selection = self.selections.newest::<usize>(cx);
|
let selection = self.selections.newest::<usize>(cx);
|
||||||
|
|
||||||
// If there is an active Diagnostic Popover. Jump to it's diagnostic instead.
|
// If there is an active Diagnostic Popover jump to its diagnostic instead.
|
||||||
if direction == Direction::Next {
|
if direction == Direction::Next {
|
||||||
if let Some(popover) = self.hover_state.diagnostic_popover.as_ref() {
|
if let Some(popover) = self.hover_state.diagnostic_popover.as_ref() {
|
||||||
let (group_id, jump_to) = popover.activation_info();
|
let (group_id, jump_to) = popover.activation_info();
|
||||||
|
@ -9743,7 +9757,7 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, _is_valid: bool) -> Ren
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
IconButton::new(("copy-block", cx.block_id), Icon::Copy)
|
IconButton::new(("copy-block", cx.block_id), IconName::Copy)
|
||||||
.icon_color(Color::Muted)
|
.icon_color(Color::Muted)
|
||||||
.size(ButtonSize::Compact)
|
.size(ButtonSize::Compact)
|
||||||
.style(ButtonStyle::Transparent)
|
.style(ButtonStyle::Transparent)
|
||||||
|
|
|
@ -539,7 +539,6 @@ fn test_clone(cx: &mut TestAppContext) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
//todo!(editor navigate)
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_navigation_history(cx: &mut TestAppContext) {
|
async fn test_navigation_history(cx: &mut TestAppContext) {
|
||||||
init_test(cx, |_| {});
|
init_test(cx, |_| {});
|
||||||
|
@ -993,7 +992,6 @@ fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
//todo!(finish editor tests)
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
|
fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
|
||||||
init_test(cx, |_| {});
|
init_test(cx, |_| {});
|
||||||
|
@ -1259,7 +1257,6 @@ fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
//todo!(finish editor tests)
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
|
fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
|
||||||
init_test(cx, |_| {});
|
init_test(cx, |_| {});
|
||||||
|
@ -1318,7 +1315,6 @@ fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
//todo!(simulate_resize)
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut gpui::TestAppContext) {
|
async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut gpui::TestAppContext) {
|
||||||
init_test(cx, |_| {});
|
init_test(cx, |_| {});
|
||||||
|
@ -2546,7 +2542,6 @@ fn test_delete_line(cx: &mut TestAppContext) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
//todo!(select_anchor_ranges)
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
|
fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
|
||||||
init_test(cx, |_| {});
|
init_test(cx, |_| {});
|
||||||
|
@ -3114,7 +3109,6 @@ fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
//todo!(test_transpose)
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
fn test_transpose(cx: &mut TestAppContext) {
|
fn test_transpose(cx: &mut TestAppContext) {
|
||||||
init_test(cx, |_| {});
|
init_test(cx, |_| {});
|
||||||
|
@ -4860,7 +4854,6 @@ async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// todo!(select_anchor_ranges)
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_snippets(cx: &mut gpui::TestAppContext) {
|
async fn test_snippets(cx: &mut gpui::TestAppContext) {
|
||||||
init_test(cx, |_| {});
|
init_test(cx, |_| {});
|
||||||
|
@ -6455,7 +6448,6 @@ fn test_highlighted_ranges(cx: &mut TestAppContext) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// todo!(following)
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_following(cx: &mut gpui::TestAppContext) {
|
async fn test_following(cx: &mut gpui::TestAppContext) {
|
||||||
init_test(cx, |_| {});
|
init_test(cx, |_| {});
|
||||||
|
@ -7094,7 +7086,6 @@ async fn test_move_to_enclosing_bracket(cx: &mut gpui::TestAppContext) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// todo!(completions)
|
|
||||||
#[gpui::test(iterations = 10)]
|
#[gpui::test(iterations = 10)]
|
||||||
async fn test_copilot(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) {
|
async fn test_copilot(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) {
|
||||||
// flaky
|
// flaky
|
||||||
|
|
|
@ -28,7 +28,7 @@ use gpui::{
|
||||||
AnchorCorner, AnyElement, AvailableSpace, BorrowWindow, Bounds, ContentMask, Corners,
|
AnchorCorner, AnyElement, AvailableSpace, BorrowWindow, Bounds, ContentMask, Corners,
|
||||||
CursorStyle, DispatchPhase, Edges, Element, ElementInputHandler, Hsla, InteractiveBounds,
|
CursorStyle, DispatchPhase, Edges, Element, ElementInputHandler, Hsla, InteractiveBounds,
|
||||||
InteractiveElement, IntoElement, ModifiersChangedEvent, MouseButton, MouseDownEvent,
|
InteractiveElement, IntoElement, ModifiersChangedEvent, MouseButton, MouseDownEvent,
|
||||||
MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, ScrollWheelEvent, ShapedLine,
|
MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, ScrollDelta, ScrollWheelEvent, ShapedLine,
|
||||||
SharedString, Size, StackingOrder, StatefulInteractiveElement, Style, Styled, TextRun,
|
SharedString, Size, StackingOrder, StatefulInteractiveElement, Style, Styled, TextRun,
|
||||||
TextStyle, View, ViewContext, WindowContext,
|
TextStyle, View, ViewContext, WindowContext,
|
||||||
};
|
};
|
||||||
|
@ -581,41 +581,6 @@ impl EditorElement {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn scroll(
|
|
||||||
editor: &mut Editor,
|
|
||||||
event: &ScrollWheelEvent,
|
|
||||||
position_map: &PositionMap,
|
|
||||||
bounds: &InteractiveBounds,
|
|
||||||
cx: &mut ViewContext<Editor>,
|
|
||||||
) {
|
|
||||||
if !bounds.visibly_contains(&event.position, cx) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let line_height = position_map.line_height;
|
|
||||||
let max_glyph_width = position_map.em_width;
|
|
||||||
let (delta, axis) = match event.delta {
|
|
||||||
gpui::ScrollDelta::Pixels(mut pixels) => {
|
|
||||||
//Trackpad
|
|
||||||
let axis = position_map.snapshot.ongoing_scroll.filter(&mut pixels);
|
|
||||||
(pixels, axis)
|
|
||||||
}
|
|
||||||
|
|
||||||
gpui::ScrollDelta::Lines(lines) => {
|
|
||||||
//Not trackpad
|
|
||||||
let pixels = point(lines.x * max_glyph_width, lines.y * line_height);
|
|
||||||
(pixels, None)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let scroll_position = position_map.snapshot.scroll_position();
|
|
||||||
let x = f32::from((scroll_position.x * max_glyph_width - delta.x) / max_glyph_width);
|
|
||||||
let y = f32::from((scroll_position.y * line_height - delta.y) / line_height);
|
|
||||||
let scroll_position = point(x, y).clamp(&point(0., 0.), &position_map.scroll_max);
|
|
||||||
editor.scroll(scroll_position, axis, cx);
|
|
||||||
cx.stop_propagation();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn paint_background(
|
fn paint_background(
|
||||||
&self,
|
&self,
|
||||||
gutter_bounds: Bounds<Pixels>,
|
gutter_bounds: Bounds<Pixels>,
|
||||||
|
@ -839,9 +804,22 @@ impl EditorElement {
|
||||||
|
|
||||||
let start_row = display_row_range.start;
|
let start_row = display_row_range.start;
|
||||||
let end_row = display_row_range.end;
|
let end_row = display_row_range.end;
|
||||||
|
// If we're in a multibuffer, row range span might include an
|
||||||
|
// excerpt header, so if we were to draw the marker straight away,
|
||||||
|
// the hunk might include the rows of that header.
|
||||||
|
// Making the range inclusive doesn't quite cut it, as we rely on the exclusivity for the soft wrap.
|
||||||
|
// Instead, we simply check whether the range we're dealing with includes
|
||||||
|
// any custom elements and if so, we stop painting the diff hunk on the first row of that custom element.
|
||||||
|
let end_row_in_current_excerpt = layout
|
||||||
|
.position_map
|
||||||
|
.snapshot
|
||||||
|
.blocks_in_range(start_row..end_row)
|
||||||
|
.next()
|
||||||
|
.map(|(start_row, _)| start_row)
|
||||||
|
.unwrap_or(end_row);
|
||||||
|
|
||||||
let start_y = start_row as f32 * line_height - scroll_top;
|
let start_y = start_row as f32 * line_height - scroll_top;
|
||||||
let end_y = end_row as f32 * line_height - scroll_top;
|
let end_y = end_row_in_current_excerpt as f32 * line_height - scroll_top;
|
||||||
|
|
||||||
let width = 0.275 * line_height;
|
let width = 0.275 * line_height;
|
||||||
let highlight_origin = bounds.origin + point(-width, start_y);
|
let highlight_origin = bounds.origin + point(-width, start_y);
|
||||||
|
@ -2450,6 +2428,64 @@ impl EditorElement {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn paint_scroll_wheel_listener(
|
||||||
|
&mut self,
|
||||||
|
interactive_bounds: &InteractiveBounds,
|
||||||
|
layout: &LayoutState,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) {
|
||||||
|
cx.on_mouse_event({
|
||||||
|
let position_map = layout.position_map.clone();
|
||||||
|
let editor = self.editor.clone();
|
||||||
|
let interactive_bounds = interactive_bounds.clone();
|
||||||
|
let mut delta = ScrollDelta::default();
|
||||||
|
|
||||||
|
move |event: &ScrollWheelEvent, phase, cx| {
|
||||||
|
if phase == DispatchPhase::Bubble
|
||||||
|
&& interactive_bounds.visibly_contains(&event.position, cx)
|
||||||
|
{
|
||||||
|
delta = delta.coalesce(event.delta);
|
||||||
|
editor.update(cx, |editor, cx| {
|
||||||
|
let position = event.position;
|
||||||
|
let position_map: &PositionMap = &position_map;
|
||||||
|
let bounds = &interactive_bounds;
|
||||||
|
if !bounds.visibly_contains(&position, cx) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let line_height = position_map.line_height;
|
||||||
|
let max_glyph_width = position_map.em_width;
|
||||||
|
let (delta, axis) = match delta {
|
||||||
|
gpui::ScrollDelta::Pixels(mut pixels) => {
|
||||||
|
//Trackpad
|
||||||
|
let axis = position_map.snapshot.ongoing_scroll.filter(&mut pixels);
|
||||||
|
(pixels, axis)
|
||||||
|
}
|
||||||
|
|
||||||
|
gpui::ScrollDelta::Lines(lines) => {
|
||||||
|
//Not trackpad
|
||||||
|
let pixels =
|
||||||
|
point(lines.x * max_glyph_width, lines.y * line_height);
|
||||||
|
(pixels, None)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let scroll_position = position_map.snapshot.scroll_position();
|
||||||
|
let x = f32::from(
|
||||||
|
(scroll_position.x * max_glyph_width - delta.x) / max_glyph_width,
|
||||||
|
);
|
||||||
|
let y =
|
||||||
|
f32::from((scroll_position.y * line_height - delta.y) / line_height);
|
||||||
|
let scroll_position =
|
||||||
|
point(x, y).clamp(&point(0., 0.), &position_map.scroll_max);
|
||||||
|
editor.scroll(scroll_position, axis, cx);
|
||||||
|
cx.stop_propagation();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
fn paint_mouse_listeners(
|
fn paint_mouse_listeners(
|
||||||
&mut self,
|
&mut self,
|
||||||
bounds: Bounds<Pixels>,
|
bounds: Bounds<Pixels>,
|
||||||
|
@ -2463,21 +2499,7 @@ impl EditorElement {
|
||||||
stacking_order: cx.stacking_order().clone(),
|
stacking_order: cx.stacking_order().clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
cx.on_mouse_event({
|
self.paint_scroll_wheel_listener(&interactive_bounds, layout, cx);
|
||||||
let position_map = layout.position_map.clone();
|
|
||||||
let editor = self.editor.clone();
|
|
||||||
let interactive_bounds = interactive_bounds.clone();
|
|
||||||
|
|
||||||
move |event: &ScrollWheelEvent, phase, cx| {
|
|
||||||
if phase == DispatchPhase::Bubble
|
|
||||||
&& interactive_bounds.visibly_contains(&event.position, cx)
|
|
||||||
{
|
|
||||||
editor.update(cx, |editor, cx| {
|
|
||||||
Self::scroll(editor, event, &position_map, &interactive_bounds, cx)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
cx.on_mouse_event({
|
cx.on_mouse_event({
|
||||||
let position_map = layout.position_map.clone();
|
let position_map = layout.position_map.clone();
|
||||||
|
|
|
@ -16,7 +16,7 @@ use lsp::DiagnosticSeverity;
|
||||||
use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart, Project};
|
use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart, Project};
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use std::{ops::Range, sync::Arc, time::Duration};
|
use std::{ops::Range, sync::Arc, time::Duration};
|
||||||
use ui::{StyledExt, Tooltip};
|
use ui::{prelude::*, Tooltip};
|
||||||
use util::TryFutureExt;
|
use util::TryFutureExt;
|
||||||
use workspace::Workspace;
|
use workspace::Workspace;
|
||||||
|
|
||||||
|
@ -514,6 +514,8 @@ impl DiagnosticPopover {
|
||||||
None => self.local_diagnostic.diagnostic.message.clone(),
|
None => self.local_diagnostic.diagnostic.message.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let status_colors = cx.theme().status();
|
||||||
|
|
||||||
struct DiagnosticColors {
|
struct DiagnosticColors {
|
||||||
pub background: Hsla,
|
pub background: Hsla,
|
||||||
pub border: Hsla,
|
pub border: Hsla,
|
||||||
|
@ -521,24 +523,24 @@ impl DiagnosticPopover {
|
||||||
|
|
||||||
let diagnostic_colors = match self.local_diagnostic.diagnostic.severity {
|
let diagnostic_colors = match self.local_diagnostic.diagnostic.severity {
|
||||||
DiagnosticSeverity::ERROR => DiagnosticColors {
|
DiagnosticSeverity::ERROR => DiagnosticColors {
|
||||||
background: style.status.error_background,
|
background: status_colors.error_background,
|
||||||
border: style.status.error_border,
|
border: status_colors.error_border,
|
||||||
},
|
},
|
||||||
DiagnosticSeverity::WARNING => DiagnosticColors {
|
DiagnosticSeverity::WARNING => DiagnosticColors {
|
||||||
background: style.status.warning_background,
|
background: status_colors.warning_background,
|
||||||
border: style.status.warning_border,
|
border: status_colors.warning_border,
|
||||||
},
|
},
|
||||||
DiagnosticSeverity::INFORMATION => DiagnosticColors {
|
DiagnosticSeverity::INFORMATION => DiagnosticColors {
|
||||||
background: style.status.info_background,
|
background: status_colors.info_background,
|
||||||
border: style.status.info_border,
|
border: status_colors.info_border,
|
||||||
},
|
},
|
||||||
DiagnosticSeverity::HINT => DiagnosticColors {
|
DiagnosticSeverity::HINT => DiagnosticColors {
|
||||||
background: style.status.hint_background,
|
background: status_colors.hint_background,
|
||||||
border: style.status.hint_border,
|
border: status_colors.hint_border,
|
||||||
},
|
},
|
||||||
_ => DiagnosticColors {
|
_ => DiagnosticColors {
|
||||||
background: style.status.ignored_background,
|
background: status_colors.ignored_background,
|
||||||
border: style.status.ignored_border,
|
border: status_colors.ignored_border,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -95,7 +95,7 @@ pub fn up_by_rows(
|
||||||
text_layout_details: &TextLayoutDetails,
|
text_layout_details: &TextLayoutDetails,
|
||||||
) -> (DisplayPoint, SelectionGoal) {
|
) -> (DisplayPoint, SelectionGoal) {
|
||||||
let mut goal_x = match goal {
|
let mut goal_x = match goal {
|
||||||
SelectionGoal::HorizontalPosition(x) => x.into(), // todo!("Can the fields in SelectionGoal by Pixels? We should extract a geometry crate and depend on that.")
|
SelectionGoal::HorizontalPosition(x) => x.into(),
|
||||||
SelectionGoal::WrappedHorizontalPosition((_, x)) => x.into(),
|
SelectionGoal::WrappedHorizontalPosition((_, x)) => x.into(),
|
||||||
SelectionGoal::HorizontalRange { end, .. } => end.into(),
|
SelectionGoal::HorizontalRange { end, .. } => end.into(),
|
||||||
_ => map.x_for_display_point(start, text_layout_details),
|
_ => map.x_for_display_point(start, text_layout_details),
|
||||||
|
|
|
@ -384,10 +384,12 @@ impl Editor {
|
||||||
) {
|
) {
|
||||||
hide_hover(self, cx);
|
hide_hover(self, cx);
|
||||||
let workspace_id = self.workspace.as_ref().map(|workspace| workspace.1);
|
let workspace_id = self.workspace.as_ref().map(|workspace| workspace.1);
|
||||||
let top_row = scroll_anchor
|
let snapshot = &self.buffer().read(cx).snapshot(cx);
|
||||||
.anchor
|
if !scroll_anchor.anchor.is_valid(snapshot) {
|
||||||
.to_point(&self.buffer().read(cx).snapshot(cx))
|
log::warn!("Invalid scroll anchor: {:?}", scroll_anchor);
|
||||||
.row;
|
return;
|
||||||
|
}
|
||||||
|
let top_row = scroll_anchor.anchor.to_point(snapshot).row;
|
||||||
self.scroll_manager
|
self.scroll_manager
|
||||||
.set_anchor(scroll_anchor, top_row, false, false, workspace_id, cx);
|
.set_anchor(scroll_anchor, top_row, false, false, workspace_id, cx);
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,10 +11,9 @@ impl Editor {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// todo!()
|
if self.mouse_context_menu.is_some() {
|
||||||
// if self.mouse_context_menu.read(cx).visible() {
|
return;
|
||||||
// return None;
|
}
|
||||||
// }
|
|
||||||
|
|
||||||
if matches!(self.mode, EditorMode::SingleLine) {
|
if matches!(self.mode, EditorMode::SingleLine) {
|
||||||
cx.propagate();
|
cx.propagate();
|
||||||
|
|
|
@ -60,8 +60,7 @@ pub fn assert_text_with_selections(
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
pub(crate) fn build_editor(buffer: Model<MultiBuffer>, cx: &mut ViewContext<Editor>) -> Editor {
|
pub(crate) fn build_editor(buffer: Model<MultiBuffer>, cx: &mut ViewContext<Editor>) -> Editor {
|
||||||
// todo!()
|
Editor::new(EditorMode::Full, buffer, None, cx)
|
||||||
Editor::new(EditorMode::Full, buffer, None, /*None,*/ cx)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn build_editor_with_project(
|
pub(crate) fn build_editor_with_project(
|
||||||
|
@ -69,6 +68,5 @@ pub(crate) fn build_editor_with_project(
|
||||||
buffer: Model<MultiBuffer>,
|
buffer: Model<MultiBuffer>,
|
||||||
cx: &mut ViewContext<Editor>,
|
cx: &mut ViewContext<Editor>,
|
||||||
) -> Editor {
|
) -> Editor {
|
||||||
// todo!()
|
Editor::new(EditorMode::Full, buffer, Some(project), cx)
|
||||||
Editor::new(EditorMode::Full, buffer, Some(project), /*None,*/ cx)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use gpui::{Render, ViewContext, WeakView};
|
use gpui::{Render, ViewContext, WeakView};
|
||||||
use ui::{prelude::*, ButtonCommon, Icon, IconButton, Tooltip};
|
use ui::{prelude::*, ButtonCommon, IconButton, IconName, Tooltip};
|
||||||
use workspace::{item::ItemHandle, StatusItemView, Workspace};
|
use workspace::{item::ItemHandle, StatusItemView, Workspace};
|
||||||
|
|
||||||
use crate::{feedback_modal::FeedbackModal, GiveFeedback};
|
use crate::{feedback_modal::FeedbackModal, GiveFeedback};
|
||||||
|
@ -27,7 +27,7 @@ impl Render for DeployFeedbackButton {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.is_some();
|
.is_some();
|
||||||
IconButton::new("give-feedback", Icon::Envelope)
|
IconButton::new("give-feedback", IconName::Envelope)
|
||||||
.style(ui::ButtonStyle::Subtle)
|
.style(ui::ButtonStyle::Subtle)
|
||||||
.icon_size(IconSize::Small)
|
.icon_size(IconSize::Small)
|
||||||
.selected(is_open)
|
.selected(is_open)
|
||||||
|
|
|
@ -7,7 +7,7 @@ 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, red, rems, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Model,
|
div, rems, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Model,
|
||||||
PromptLevel, Render, Task, View, ViewContext,
|
PromptLevel, Render, Task, View, ViewContext,
|
||||||
};
|
};
|
||||||
use isahc::Request;
|
use isahc::Request;
|
||||||
|
@ -179,14 +179,13 @@ impl FeedbackModal {
|
||||||
editor
|
editor
|
||||||
});
|
});
|
||||||
|
|
||||||
// Moved here because providing it inline breaks rustfmt
|
|
||||||
let placeholder_text =
|
|
||||||
"You can use markdown to organize your feedback with code and links.";
|
|
||||||
|
|
||||||
let feedback_editor = cx.new_view(|cx| {
|
let feedback_editor = cx.new_view(|cx| {
|
||||||
let mut editor = Editor::for_buffer(buffer, Some(project.clone()), cx);
|
let mut editor = Editor::for_buffer(buffer, Some(project.clone()), cx);
|
||||||
editor.set_placeholder_text(placeholder_text, cx);
|
editor.set_placeholder_text(
|
||||||
// editor.set_show_gutter(false, cx);
|
"You can use markdown to organize your feedback with code and links.",
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
editor.set_show_gutter(false, cx);
|
||||||
editor.set_vertical_scroll_margin(5, cx);
|
editor.set_vertical_scroll_margin(5, cx);
|
||||||
editor
|
editor
|
||||||
});
|
});
|
||||||
|
@ -422,10 +421,6 @@ impl Render for FeedbackModal {
|
||||||
let open_community_repo =
|
let open_community_repo =
|
||||||
cx.listener(|_, _, cx| cx.dispatch_action(Box::new(OpenZedCommunityRepo)));
|
cx.listener(|_, _, cx| cx.dispatch_action(Box::new(OpenZedCommunityRepo)));
|
||||||
|
|
||||||
// Moved this here because providing it inline breaks rustfmt
|
|
||||||
let provide_an_email_address =
|
|
||||||
"Provide an email address if you want us to be able to reply.";
|
|
||||||
|
|
||||||
v_stack()
|
v_stack()
|
||||||
.elevation_3(cx)
|
.elevation_3(cx)
|
||||||
.key_context("GiveFeedback")
|
.key_context("GiveFeedback")
|
||||||
|
@ -434,11 +429,8 @@ impl Render for FeedbackModal {
|
||||||
.max_w(rems(96.))
|
.max_w(rems(96.))
|
||||||
.h(rems(32.))
|
.h(rems(32.))
|
||||||
.p_4()
|
.p_4()
|
||||||
.gap_4()
|
.gap_2()
|
||||||
.child(v_stack().child(
|
.child(Headline::new("Share Feedback"))
|
||||||
// TODO: Add Headline component to `ui2`
|
|
||||||
div().text_xl().child("Share Feedback"),
|
|
||||||
))
|
|
||||||
.child(
|
.child(
|
||||||
Label::new(if self.character_count < *FEEDBACK_CHAR_LIMIT.start() {
|
Label::new(if self.character_count < *FEEDBACK_CHAR_LIMIT.start() {
|
||||||
format!(
|
format!(
|
||||||
|
@ -468,17 +460,26 @@ impl Render for FeedbackModal {
|
||||||
.child(self.feedback_editor.clone()),
|
.child(self.feedback_editor.clone()),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
h_stack()
|
v_stack()
|
||||||
.bg(cx.theme().colors().editor_background)
|
.gap_1()
|
||||||
.p_2()
|
.child(
|
||||||
.border()
|
h_stack()
|
||||||
.rounded_md()
|
.bg(cx.theme().colors().editor_background)
|
||||||
.border_color(if self.valid_email_address() {
|
.p_2()
|
||||||
cx.theme().colors().border
|
.border()
|
||||||
} else {
|
.rounded_md()
|
||||||
red()
|
.border_color(if self.valid_email_address() {
|
||||||
})
|
cx.theme().colors().border
|
||||||
.child(self.email_address_editor.clone()),
|
} else {
|
||||||
|
cx.theme().status().error_border
|
||||||
|
})
|
||||||
|
.child(self.email_address_editor.clone()),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
Label::new("Provide an email address if you want us to be able to reply.")
|
||||||
|
.size(LabelSize::Small)
|
||||||
|
.color(Color::Muted),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
h_stack()
|
h_stack()
|
||||||
|
@ -487,7 +488,7 @@ impl Render for FeedbackModal {
|
||||||
.child(
|
.child(
|
||||||
Button::new("community_repository", "Community Repository")
|
Button::new("community_repository", "Community Repository")
|
||||||
.style(ButtonStyle::Transparent)
|
.style(ButtonStyle::Transparent)
|
||||||
.icon(Icon::ExternalLink)
|
.icon(IconName::ExternalLink)
|
||||||
.icon_position(IconPosition::End)
|
.icon_position(IconPosition::End)
|
||||||
.icon_size(IconSize::Small)
|
.icon_size(IconSize::Small)
|
||||||
.on_click(open_community_repo),
|
.on_click(open_community_repo),
|
||||||
|
@ -515,12 +516,7 @@ impl Render for FeedbackModal {
|
||||||
this.submit(cx).detach();
|
this.submit(cx).detach();
|
||||||
}))
|
}))
|
||||||
.tooltip(move |cx| {
|
.tooltip(move |cx| {
|
||||||
Tooltip::with_meta(
|
Tooltip::text("Submit feedback to the Zed team.", cx)
|
||||||
"Submit feedback to the Zed team.",
|
|
||||||
None,
|
|
||||||
provide_an_email_address,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
.when(!self.can_submit(), |this| this.disabled(true)),
|
.when(!self.can_submit(), |this| this.disabled(true)),
|
||||||
),
|
),
|
||||||
|
|
|
@ -1297,7 +1297,7 @@ mod tests {
|
||||||
// so that one should be sorted earlier
|
// so that one should be sorted earlier
|
||||||
let b_path = ProjectPath {
|
let b_path = ProjectPath {
|
||||||
worktree_id,
|
worktree_id,
|
||||||
path: Arc::from(Path::new("/root/dir2/b.txt")),
|
path: Arc::from(Path::new("dir2/b.txt")),
|
||||||
};
|
};
|
||||||
workspace
|
workspace
|
||||||
.update(cx, |workspace, cx| {
|
.update(cx, |workspace, cx| {
|
||||||
|
|
|
@ -50,7 +50,7 @@ impl Render for Menu {
|
||||||
.on_action(|this, move: &MoveDown, cx| {
|
.on_action(|this, move: &MoveDown, cx| {
|
||||||
// ...
|
// ...
|
||||||
})
|
})
|
||||||
.children(todo!())
|
.children(unimplemented!())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
@ -68,7 +68,7 @@ impl Render for Menu {
|
||||||
.on_action(|this, move: &MoveDown, cx| {
|
.on_action(|this, move: &MoveDown, cx| {
|
||||||
// ...
|
// ...
|
||||||
})
|
})
|
||||||
.children(todo!())
|
.children(unimplemented!())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
|
@ -104,7 +104,7 @@ pub struct ActionData {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This constant must be public to be accessible from other crates.
|
/// This constant must be public to be accessible from other crates.
|
||||||
/// But it's existence is an implementation detail and should not be used directly.
|
/// But its existence is an implementation detail and should not be used directly.
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
#[linkme::distributed_slice]
|
#[linkme::distributed_slice]
|
||||||
pub static __GPUI_ACTIONS: [MacroActionBuilder];
|
pub static __GPUI_ACTIONS: [MacroActionBuilder];
|
||||||
|
@ -114,14 +114,26 @@ impl ActionRegistry {
|
||||||
pub(crate) fn load_actions(&mut self) {
|
pub(crate) fn load_actions(&mut self) {
|
||||||
for builder in __GPUI_ACTIONS {
|
for builder in __GPUI_ACTIONS {
|
||||||
let action = builder();
|
let action = builder();
|
||||||
//todo(remove)
|
self.insert_action(action);
|
||||||
let name: SharedString = action.name.into();
|
|
||||||
self.builders_by_name.insert(name.clone(), action.build);
|
|
||||||
self.names_by_type_id.insert(action.type_id, name.clone());
|
|
||||||
self.all_names.push(name);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(crate) fn load_action<A: Action>(&mut self) {
|
||||||
|
self.insert_action(ActionData {
|
||||||
|
name: A::debug_name(),
|
||||||
|
type_id: TypeId::of::<A>(),
|
||||||
|
build: A::build,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insert_action(&mut self, action: ActionData) {
|
||||||
|
let name: SharedString = action.name.into();
|
||||||
|
self.builders_by_name.insert(name.clone(), action.build);
|
||||||
|
self.names_by_type_id.insert(action.type_id, name.clone());
|
||||||
|
self.all_names.push(name);
|
||||||
|
}
|
||||||
|
|
||||||
/// Construct an action based on its name and optional JSON parameters sourced from the keymap.
|
/// Construct an action based on its name and optional JSON parameters sourced from the keymap.
|
||||||
pub fn build_action_type(&self, type_id: &TypeId) -> Result<Box<dyn Action>> {
|
pub fn build_action_type(&self, type_id: &TypeId) -> Result<Box<dyn Action>> {
|
||||||
let name = self
|
let name = self
|
||||||
|
@ -203,7 +215,6 @@ macro_rules! __impl_action {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// todo!() why is this needed in addition to name?
|
|
||||||
fn debug_name() -> &'static str
|
fn debug_name() -> &'static str
|
||||||
where
|
where
|
||||||
Self: ::std::marker::Sized
|
Self: ::std::marker::Sized
|
||||||
|
|
|
@ -45,11 +45,13 @@ use util::{
|
||||||
|
|
||||||
/// Temporary(?) wrapper around [`RefCell<AppContext>`] to help us debug any double borrows.
|
/// Temporary(?) wrapper around [`RefCell<AppContext>`] to help us debug any double borrows.
|
||||||
/// Strongly consider removing after stabilization.
|
/// Strongly consider removing after stabilization.
|
||||||
|
#[doc(hidden)]
|
||||||
pub struct AppCell {
|
pub struct AppCell {
|
||||||
app: RefCell<AppContext>,
|
app: RefCell<AppContext>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppCell {
|
impl AppCell {
|
||||||
|
#[doc(hidden)]
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
pub fn borrow(&self) -> AppRef {
|
pub fn borrow(&self) -> AppRef {
|
||||||
if option_env!("TRACK_THREAD_BORROWS").is_some() {
|
if option_env!("TRACK_THREAD_BORROWS").is_some() {
|
||||||
|
@ -59,6 +61,7 @@ impl AppCell {
|
||||||
AppRef(self.app.borrow())
|
AppRef(self.app.borrow())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
pub fn borrow_mut(&self) -> AppRefMut {
|
pub fn borrow_mut(&self) -> AppRefMut {
|
||||||
if option_env!("TRACK_THREAD_BORROWS").is_some() {
|
if option_env!("TRACK_THREAD_BORROWS").is_some() {
|
||||||
|
@ -69,6 +72,7 @@ impl AppCell {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
#[derive(Deref, DerefMut)]
|
#[derive(Deref, DerefMut)]
|
||||||
pub struct AppRef<'a>(Ref<'a, AppContext>);
|
pub struct AppRef<'a>(Ref<'a, AppContext>);
|
||||||
|
|
||||||
|
@ -81,6 +85,7 @@ impl<'a> Drop for AppRef<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
#[derive(Deref, DerefMut)]
|
#[derive(Deref, DerefMut)]
|
||||||
pub struct AppRefMut<'a>(RefMut<'a, AppContext>);
|
pub struct AppRefMut<'a>(RefMut<'a, AppContext>);
|
||||||
|
|
||||||
|
@ -93,6 +98,8 @@ impl<'a> Drop for AppRefMut<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A reference to a GPUI application, typically constructed in the `main` function of your app.
|
||||||
|
/// You won't interact with this type much outside of initial configuration and startup.
|
||||||
pub struct App(Rc<AppCell>);
|
pub struct App(Rc<AppCell>);
|
||||||
|
|
||||||
/// Represents an application before it is fully launched. Once your app is
|
/// Represents an application before it is fully launched. Once your app is
|
||||||
|
@ -136,6 +143,8 @@ impl App {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Invokes a handler when an already-running application is launched.
|
||||||
|
/// On macOS, this can occur when the application icon is double-clicked or the app is launched via the dock.
|
||||||
pub fn on_reopen<F>(&self, mut callback: F) -> &Self
|
pub fn on_reopen<F>(&self, mut callback: F) -> &Self
|
||||||
where
|
where
|
||||||
F: 'static + FnMut(&mut AppContext),
|
F: 'static + FnMut(&mut AppContext),
|
||||||
|
@ -149,18 +158,22 @@ impl App {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns metadata associated with the application
|
||||||
pub fn metadata(&self) -> AppMetadata {
|
pub fn metadata(&self) -> AppMetadata {
|
||||||
self.0.borrow().app_metadata.clone()
|
self.0.borrow().app_metadata.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns a handle to the [`BackgroundExecutor`] associated with this app, which can be used to spawn futures in the background.
|
||||||
pub fn background_executor(&self) -> BackgroundExecutor {
|
pub fn background_executor(&self) -> BackgroundExecutor {
|
||||||
self.0.borrow().background_executor.clone()
|
self.0.borrow().background_executor.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns a handle to the [`ForegroundExecutor`] associated with this app, which can be used to spawn futures in the foreground.
|
||||||
pub fn foreground_executor(&self) -> ForegroundExecutor {
|
pub fn foreground_executor(&self) -> ForegroundExecutor {
|
||||||
self.0.borrow().foreground_executor.clone()
|
self.0.borrow().foreground_executor.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns a reference to the [`TextSystem`] associated with this app.
|
||||||
pub fn text_system(&self) -> Arc<TextSystem> {
|
pub fn text_system(&self) -> Arc<TextSystem> {
|
||||||
self.0.borrow().text_system.clone()
|
self.0.borrow().text_system.clone()
|
||||||
}
|
}
|
||||||
|
@ -174,12 +187,6 @@ type QuitHandler = Box<dyn FnOnce(&mut AppContext) -> LocalBoxFuture<'static, ()
|
||||||
type ReleaseListener = Box<dyn FnOnce(&mut dyn Any, &mut AppContext) + 'static>;
|
type ReleaseListener = Box<dyn FnOnce(&mut dyn Any, &mut AppContext) + 'static>;
|
||||||
type NewViewListener = Box<dyn FnMut(AnyView, &mut WindowContext) + 'static>;
|
type NewViewListener = Box<dyn FnMut(AnyView, &mut WindowContext) + 'static>;
|
||||||
|
|
||||||
// struct FrameConsumer {
|
|
||||||
// next_frame_callbacks: Vec<FrameCallback>,
|
|
||||||
// task: Task<()>,
|
|
||||||
// display_linker
|
|
||||||
// }
|
|
||||||
|
|
||||||
pub struct AppContext {
|
pub struct AppContext {
|
||||||
pub(crate) this: Weak<AppCell>,
|
pub(crate) this: Weak<AppCell>,
|
||||||
pub(crate) platform: Rc<dyn Platform>,
|
pub(crate) platform: Rc<dyn Platform>,
|
||||||
|
@ -292,7 +299,7 @@ impl AppContext {
|
||||||
app
|
app
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Quit the application gracefully. Handlers registered with `ModelContext::on_app_quit`
|
/// Quit the application gracefully. Handlers registered with [`ModelContext::on_app_quit`]
|
||||||
/// will be given 100ms to complete before exiting.
|
/// will be given 100ms to complete before exiting.
|
||||||
pub fn shutdown(&mut self) {
|
pub fn shutdown(&mut self) {
|
||||||
let mut futures = Vec::new();
|
let mut futures = Vec::new();
|
||||||
|
@ -314,10 +321,12 @@ impl AppContext {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Gracefully quit the application via the platform's standard routine.
|
||||||
pub fn quit(&mut self) {
|
pub fn quit(&mut self) {
|
||||||
self.platform.quit();
|
self.platform.quit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get metadata about the app and platform.
|
||||||
pub fn app_metadata(&self) -> AppMetadata {
|
pub fn app_metadata(&self) -> AppMetadata {
|
||||||
self.app_metadata.clone()
|
self.app_metadata.clone()
|
||||||
}
|
}
|
||||||
|
@ -340,6 +349,7 @@ impl AppContext {
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Arrange a callback to be invoked when the given model or view calls `notify` on its respective context.
|
||||||
pub fn observe<W, E>(
|
pub fn observe<W, E>(
|
||||||
&mut self,
|
&mut self,
|
||||||
entity: &E,
|
entity: &E,
|
||||||
|
@ -355,7 +365,7 @@ impl AppContext {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn observe_internal<W, E>(
|
pub(crate) fn observe_internal<W, E>(
|
||||||
&mut self,
|
&mut self,
|
||||||
entity: &E,
|
entity: &E,
|
||||||
mut on_notify: impl FnMut(E, &mut AppContext) -> bool + 'static,
|
mut on_notify: impl FnMut(E, &mut AppContext) -> bool + 'static,
|
||||||
|
@ -380,15 +390,17 @@ impl AppContext {
|
||||||
subscription
|
subscription
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn subscribe<T, E, Evt>(
|
/// Arrange for the given callback to be invoked whenever the given model or view emits an event of a given type.
|
||||||
|
/// The callback is provided a handle to the emitting entity and a reference to the emitted event.
|
||||||
|
pub fn subscribe<T, E, Event>(
|
||||||
&mut self,
|
&mut self,
|
||||||
entity: &E,
|
entity: &E,
|
||||||
mut on_event: impl FnMut(E, &Evt, &mut AppContext) + 'static,
|
mut on_event: impl FnMut(E, &Event, &mut AppContext) + 'static,
|
||||||
) -> Subscription
|
) -> Subscription
|
||||||
where
|
where
|
||||||
T: 'static + EventEmitter<Evt>,
|
T: 'static + EventEmitter<Event>,
|
||||||
E: Entity<T>,
|
E: Entity<T>,
|
||||||
Evt: 'static,
|
Event: 'static,
|
||||||
{
|
{
|
||||||
self.subscribe_internal(entity, move |entity, event, cx| {
|
self.subscribe_internal(entity, move |entity, event, cx| {
|
||||||
on_event(entity, event, cx);
|
on_event(entity, event, cx);
|
||||||
|
@ -426,6 +438,9 @@ impl AppContext {
|
||||||
subscription
|
subscription
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns handles to all open windows in the application.
|
||||||
|
/// Each handle could be downcast to a handle typed for the root view of that window.
|
||||||
|
/// To find all windows of a given type, you could filter on
|
||||||
pub fn windows(&self) -> Vec<AnyWindowHandle> {
|
pub fn windows(&self) -> Vec<AnyWindowHandle> {
|
||||||
self.windows
|
self.windows
|
||||||
.values()
|
.values()
|
||||||
|
@ -565,7 +580,7 @@ impl AppContext {
|
||||||
self.pending_effects.push_back(effect);
|
self.pending_effects.push_back(effect);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Called at the end of AppContext::update to complete any side effects
|
/// Called at the end of [`AppContext::update`] to complete any side effects
|
||||||
/// such as notifying observers, emitting events, etc. Effects can themselves
|
/// such as notifying observers, emitting events, etc. Effects can themselves
|
||||||
/// cause effects, so we continue looping until all effects are processed.
|
/// cause effects, so we continue looping until all effects are processed.
|
||||||
fn flush_effects(&mut self) {
|
fn flush_effects(&mut self) {
|
||||||
|
|
|
@ -82,6 +82,7 @@ impl Context for AsyncAppContext {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AsyncAppContext {
|
impl AsyncAppContext {
|
||||||
|
/// Schedules all windows in the application to be redrawn.
|
||||||
pub fn refresh(&mut self) -> Result<()> {
|
pub fn refresh(&mut self) -> Result<()> {
|
||||||
let app = self
|
let app = self
|
||||||
.app
|
.app
|
||||||
|
@ -92,14 +93,17 @@ impl AsyncAppContext {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get an executor which can be used to spawn futures in the background.
|
||||||
pub fn background_executor(&self) -> &BackgroundExecutor {
|
pub fn background_executor(&self) -> &BackgroundExecutor {
|
||||||
&self.background_executor
|
&self.background_executor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get an executor which can be used to spawn futures in the foreground.
|
||||||
pub fn foreground_executor(&self) -> &ForegroundExecutor {
|
pub fn foreground_executor(&self) -> &ForegroundExecutor {
|
||||||
&self.foreground_executor
|
&self.foreground_executor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Invoke the given function in the context of the app, then flush any effects produced during its invocation.
|
||||||
pub fn update<R>(&self, f: impl FnOnce(&mut AppContext) -> R) -> Result<R> {
|
pub fn update<R>(&self, f: impl FnOnce(&mut AppContext) -> R) -> Result<R> {
|
||||||
let app = self
|
let app = self
|
||||||
.app
|
.app
|
||||||
|
@ -109,6 +113,7 @@ impl AsyncAppContext {
|
||||||
Ok(f(&mut lock))
|
Ok(f(&mut lock))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Open a window with the given options based on the root view returned by the given function.
|
||||||
pub fn open_window<V>(
|
pub fn open_window<V>(
|
||||||
&self,
|
&self,
|
||||||
options: crate::WindowOptions,
|
options: crate::WindowOptions,
|
||||||
|
@ -125,6 +130,7 @@ impl AsyncAppContext {
|
||||||
Ok(lock.open_window(options, build_root_view))
|
Ok(lock.open_window(options, build_root_view))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Schedule a future to be polled in the background.
|
||||||
pub fn spawn<Fut, R>(&self, f: impl FnOnce(AsyncAppContext) -> Fut) -> Task<R>
|
pub fn spawn<Fut, R>(&self, f: impl FnOnce(AsyncAppContext) -> Fut) -> Task<R>
|
||||||
where
|
where
|
||||||
Fut: Future<Output = R> + 'static,
|
Fut: Future<Output = R> + 'static,
|
||||||
|
|
|
@ -19,7 +19,10 @@ use std::{
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
|
|
||||||
slotmap::new_key_type! { pub struct EntityId; }
|
slotmap::new_key_type! {
|
||||||
|
/// A unique identifier for a model or view across the application.
|
||||||
|
pub struct EntityId;
|
||||||
|
}
|
||||||
|
|
||||||
impl EntityId {
|
impl EntityId {
|
||||||
pub fn as_u64(self) -> u64 {
|
pub fn as_u64(self) -> u64 {
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#![deny(missing_docs)]
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
div, Action, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext,
|
div, Action, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext,
|
||||||
BackgroundExecutor, ClipboardItem, Context, Entity, EventEmitter, ForegroundExecutor,
|
BackgroundExecutor, ClipboardItem, Context, Entity, EventEmitter, ForegroundExecutor,
|
||||||
|
@ -9,14 +11,21 @@ use anyhow::{anyhow, bail};
|
||||||
use futures::{Stream, StreamExt};
|
use futures::{Stream, StreamExt};
|
||||||
use std::{future::Future, ops::Deref, rc::Rc, sync::Arc, time::Duration};
|
use std::{future::Future, ops::Deref, rc::Rc, sync::Arc, time::Duration};
|
||||||
|
|
||||||
|
/// A TestAppContext is provided to tests created with `#[gpui::test]`, it provides
|
||||||
|
/// an implementation of `Context` with additional methods that are useful in tests.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct TestAppContext {
|
pub struct TestAppContext {
|
||||||
|
#[doc(hidden)]
|
||||||
pub app: Rc<AppCell>,
|
pub app: Rc<AppCell>,
|
||||||
|
#[doc(hidden)]
|
||||||
pub background_executor: BackgroundExecutor,
|
pub background_executor: BackgroundExecutor,
|
||||||
|
#[doc(hidden)]
|
||||||
pub foreground_executor: ForegroundExecutor,
|
pub foreground_executor: ForegroundExecutor,
|
||||||
|
#[doc(hidden)]
|
||||||
pub dispatcher: TestDispatcher,
|
pub dispatcher: TestDispatcher,
|
||||||
pub test_platform: Rc<TestPlatform>,
|
test_platform: Rc<TestPlatform>,
|
||||||
text_system: Arc<TextSystem>,
|
text_system: Arc<TextSystem>,
|
||||||
|
fn_name: Option<&'static str>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Context for TestAppContext {
|
impl Context for TestAppContext {
|
||||||
|
@ -76,7 +85,8 @@ impl Context for TestAppContext {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TestAppContext {
|
impl TestAppContext {
|
||||||
pub fn new(dispatcher: TestDispatcher) -> Self {
|
/// Creates a new `TestAppContext`. Usually you can rely on `#[gpui::test]` to do this for you.
|
||||||
|
pub fn new(dispatcher: TestDispatcher, fn_name: Option<&'static str>) -> Self {
|
||||||
let arc_dispatcher = Arc::new(dispatcher.clone());
|
let arc_dispatcher = Arc::new(dispatcher.clone());
|
||||||
let background_executor = BackgroundExecutor::new(arc_dispatcher.clone());
|
let background_executor = BackgroundExecutor::new(arc_dispatcher.clone());
|
||||||
let foreground_executor = ForegroundExecutor::new(arc_dispatcher);
|
let foreground_executor = ForegroundExecutor::new(arc_dispatcher);
|
||||||
|
@ -92,41 +102,61 @@ impl TestAppContext {
|
||||||
dispatcher: dispatcher.clone(),
|
dispatcher: dispatcher.clone(),
|
||||||
test_platform: platform,
|
test_platform: platform,
|
||||||
text_system,
|
text_system,
|
||||||
|
fn_name,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new_app(&self) -> TestAppContext {
|
/// The name of the test function that created this `TestAppContext`
|
||||||
Self::new(self.dispatcher.clone())
|
pub fn test_function_name(&self) -> Option<&'static str> {
|
||||||
|
self.fn_name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Checks whether there have been any new path prompts received by the platform.
|
||||||
|
pub fn did_prompt_for_new_path(&self) -> bool {
|
||||||
|
self.test_platform.did_prompt_for_new_path()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// returns a new `TestAppContext` re-using the same executors to interleave tasks.
|
||||||
|
pub fn new_app(&self) -> TestAppContext {
|
||||||
|
Self::new(self.dispatcher.clone(), self.fn_name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simulates quitting the app.
|
||||||
pub fn quit(&self) {
|
pub fn quit(&self) {
|
||||||
self.app.borrow_mut().shutdown();
|
self.app.borrow_mut().shutdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Schedules all windows to be redrawn on the next effect cycle.
|
||||||
pub fn refresh(&mut self) -> Result<()> {
|
pub fn refresh(&mut self) -> Result<()> {
|
||||||
let mut app = self.app.borrow_mut();
|
let mut app = self.app.borrow_mut();
|
||||||
app.refresh();
|
app.refresh();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns an executor (for running tasks in the background)
|
||||||
pub fn executor(&self) -> BackgroundExecutor {
|
pub fn executor(&self) -> BackgroundExecutor {
|
||||||
self.background_executor.clone()
|
self.background_executor.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns an executor (for running tasks on the main thread)
|
||||||
pub fn foreground_executor(&self) -> &ForegroundExecutor {
|
pub fn foreground_executor(&self) -> &ForegroundExecutor {
|
||||||
&self.foreground_executor
|
&self.foreground_executor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Gives you an `&mut AppContext` for the duration of the closure
|
||||||
pub fn update<R>(&self, f: impl FnOnce(&mut AppContext) -> R) -> R {
|
pub fn update<R>(&self, f: impl FnOnce(&mut AppContext) -> R) -> R {
|
||||||
let mut cx = self.app.borrow_mut();
|
let mut cx = self.app.borrow_mut();
|
||||||
cx.update(f)
|
cx.update(f)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Gives you an `&AppContext` for the duration of the closure
|
||||||
pub fn read<R>(&self, f: impl FnOnce(&AppContext) -> R) -> R {
|
pub fn read<R>(&self, f: impl FnOnce(&AppContext) -> R) -> R {
|
||||||
let cx = self.app.borrow();
|
let cx = self.app.borrow();
|
||||||
f(&*cx)
|
f(&*cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Adds a new window. The Window will always be backed by a `TestWindow` which
|
||||||
|
/// can be retrieved with `self.test_window(handle)`
|
||||||
pub fn add_window<F, V>(&mut self, build_window: F) -> WindowHandle<V>
|
pub fn add_window<F, V>(&mut self, build_window: F) -> WindowHandle<V>
|
||||||
where
|
where
|
||||||
F: FnOnce(&mut ViewContext<V>) -> V,
|
F: FnOnce(&mut ViewContext<V>) -> V,
|
||||||
|
@ -136,12 +166,16 @@ impl TestAppContext {
|
||||||
cx.open_window(WindowOptions::default(), |cx| cx.new_view(build_window))
|
cx.open_window(WindowOptions::default(), |cx| cx.new_view(build_window))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Adds a new window with no content.
|
||||||
pub fn add_empty_window(&mut self) -> AnyWindowHandle {
|
pub fn add_empty_window(&mut self) -> AnyWindowHandle {
|
||||||
let mut cx = self.app.borrow_mut();
|
let mut cx = self.app.borrow_mut();
|
||||||
cx.open_window(WindowOptions::default(), |cx| cx.new_view(|_| EmptyView {}))
|
cx.open_window(WindowOptions::default(), |cx| cx.new_view(|_| EmptyView {}))
|
||||||
.any_handle
|
.any_handle
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Adds a new window, and returns its root view and a `VisualTestContext` which can be used
|
||||||
|
/// as a `WindowContext` for the rest of the test. Typically you would shadow this context with
|
||||||
|
/// the returned one. `let (view, cx) = cx.add_window_view(...);`
|
||||||
pub fn add_window_view<F, V>(&mut self, build_window: F) -> (View<V>, &mut VisualTestContext)
|
pub fn add_window_view<F, V>(&mut self, build_window: F) -> (View<V>, &mut VisualTestContext)
|
||||||
where
|
where
|
||||||
F: FnOnce(&mut ViewContext<V>) -> V,
|
F: FnOnce(&mut ViewContext<V>) -> V,
|
||||||
|
@ -152,22 +186,28 @@ impl TestAppContext {
|
||||||
drop(cx);
|
drop(cx);
|
||||||
let view = window.root_view(self).unwrap();
|
let view = window.root_view(self).unwrap();
|
||||||
let cx = Box::new(VisualTestContext::from_window(*window.deref(), self));
|
let cx = Box::new(VisualTestContext::from_window(*window.deref(), self));
|
||||||
|
cx.run_until_parked();
|
||||||
// it might be nice to try and cleanup these at the end of each test.
|
// it might be nice to try and cleanup these at the end of each test.
|
||||||
(view, Box::leak(cx))
|
(view, Box::leak(cx))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// returns the TextSystem
|
||||||
pub fn text_system(&self) -> &Arc<TextSystem> {
|
pub fn text_system(&self) -> &Arc<TextSystem> {
|
||||||
&self.text_system
|
&self.text_system
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Simulates writing to the platform clipboard
|
||||||
pub fn write_to_clipboard(&self, item: ClipboardItem) {
|
pub fn write_to_clipboard(&self, item: ClipboardItem) {
|
||||||
self.test_platform.write_to_clipboard(item)
|
self.test_platform.write_to_clipboard(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Simulates reading from the platform clipboard.
|
||||||
|
/// This will return the most recent value from `write_to_clipboard`.
|
||||||
pub fn read_from_clipboard(&self) -> Option<ClipboardItem> {
|
pub fn read_from_clipboard(&self) -> Option<ClipboardItem> {
|
||||||
self.test_platform.read_from_clipboard()
|
self.test_platform.read_from_clipboard()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Simulates choosing a File in the platform's "Open" dialog.
|
||||||
pub fn simulate_new_path_selection(
|
pub fn simulate_new_path_selection(
|
||||||
&self,
|
&self,
|
||||||
select_path: impl FnOnce(&std::path::Path) -> Option<std::path::PathBuf>,
|
select_path: impl FnOnce(&std::path::Path) -> Option<std::path::PathBuf>,
|
||||||
|
@ -175,22 +215,27 @@ impl TestAppContext {
|
||||||
self.test_platform.simulate_new_path_selection(select_path);
|
self.test_platform.simulate_new_path_selection(select_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Simulates clicking a button in an platform-level alert dialog.
|
||||||
pub fn simulate_prompt_answer(&self, button_ix: usize) {
|
pub fn simulate_prompt_answer(&self, button_ix: usize) {
|
||||||
self.test_platform.simulate_prompt_answer(button_ix);
|
self.test_platform.simulate_prompt_answer(button_ix);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns true if there's an alert dialog open.
|
||||||
pub fn has_pending_prompt(&self) -> bool {
|
pub fn has_pending_prompt(&self) -> bool {
|
||||||
self.test_platform.has_pending_prompt()
|
self.test_platform.has_pending_prompt()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Simulates the user resizing the window to the new size.
|
||||||
pub fn simulate_window_resize(&self, window_handle: AnyWindowHandle, size: Size<Pixels>) {
|
pub fn simulate_window_resize(&self, window_handle: AnyWindowHandle, size: Size<Pixels>) {
|
||||||
self.test_window(window_handle).simulate_resize(size);
|
self.test_window(window_handle).simulate_resize(size);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns all windows open in the test.
|
||||||
pub fn windows(&self) -> Vec<AnyWindowHandle> {
|
pub fn windows(&self) -> Vec<AnyWindowHandle> {
|
||||||
self.app.borrow().windows().clone()
|
self.app.borrow().windows().clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Run the given task on the main thread.
|
||||||
pub fn spawn<Fut, R>(&self, f: impl FnOnce(AsyncAppContext) -> Fut) -> Task<R>
|
pub fn spawn<Fut, R>(&self, f: impl FnOnce(AsyncAppContext) -> Fut) -> Task<R>
|
||||||
where
|
where
|
||||||
Fut: Future<Output = R> + 'static,
|
Fut: Future<Output = R> + 'static,
|
||||||
|
@ -199,16 +244,20 @@ impl TestAppContext {
|
||||||
self.foreground_executor.spawn(f(self.to_async()))
|
self.foreground_executor.spawn(f(self.to_async()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// true if the given global is defined
|
||||||
pub fn has_global<G: 'static>(&self) -> bool {
|
pub fn has_global<G: 'static>(&self) -> bool {
|
||||||
let app = self.app.borrow();
|
let app = self.app.borrow();
|
||||||
app.has_global::<G>()
|
app.has_global::<G>()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// runs the given closure with a reference to the global
|
||||||
|
/// panics if `has_global` would return false.
|
||||||
pub fn read_global<G: 'static, R>(&self, read: impl FnOnce(&G, &AppContext) -> R) -> R {
|
pub fn read_global<G: 'static, R>(&self, read: impl FnOnce(&G, &AppContext) -> R) -> R {
|
||||||
let app = self.app.borrow();
|
let app = self.app.borrow();
|
||||||
read(app.global(), &app)
|
read(app.global(), &app)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// runs the given closure with a reference to the global (if set)
|
||||||
pub fn try_read_global<G: 'static, R>(
|
pub fn try_read_global<G: 'static, R>(
|
||||||
&self,
|
&self,
|
||||||
read: impl FnOnce(&G, &AppContext) -> R,
|
read: impl FnOnce(&G, &AppContext) -> R,
|
||||||
|
@ -217,11 +266,13 @@ impl TestAppContext {
|
||||||
Some(read(lock.try_global()?, &lock))
|
Some(read(lock.try_global()?, &lock))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// sets the global in this context.
|
||||||
pub fn set_global<G: 'static>(&mut self, global: G) {
|
pub fn set_global<G: 'static>(&mut self, global: G) {
|
||||||
let mut lock = self.app.borrow_mut();
|
let mut lock = self.app.borrow_mut();
|
||||||
lock.set_global(global);
|
lock.set_global(global);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// updates the global in this context. (panics if `has_global` would return false)
|
||||||
pub fn update_global<G: 'static, R>(
|
pub fn update_global<G: 'static, R>(
|
||||||
&mut self,
|
&mut self,
|
||||||
update: impl FnOnce(&mut G, &mut AppContext) -> R,
|
update: impl FnOnce(&mut G, &mut AppContext) -> R,
|
||||||
|
@ -230,6 +281,8 @@ impl TestAppContext {
|
||||||
lock.update_global(update)
|
lock.update_global(update)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns an `AsyncAppContext` which can be used to run tasks that expect to be on a background
|
||||||
|
/// thread on the current thread in tests.
|
||||||
pub fn to_async(&self) -> AsyncAppContext {
|
pub fn to_async(&self) -> AsyncAppContext {
|
||||||
AsyncAppContext {
|
AsyncAppContext {
|
||||||
app: Rc::downgrade(&self.app),
|
app: Rc::downgrade(&self.app),
|
||||||
|
@ -238,6 +291,12 @@ impl TestAppContext {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Wait until there are no more pending tasks.
|
||||||
|
pub fn run_until_parked(&mut self) {
|
||||||
|
self.background_executor.run_until_parked()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simulate dispatching an action to the currently focused node in the window.
|
||||||
pub fn dispatch_action<A>(&mut self, window: AnyWindowHandle, action: A)
|
pub fn dispatch_action<A>(&mut self, window: AnyWindowHandle, action: A)
|
||||||
where
|
where
|
||||||
A: Action,
|
A: Action,
|
||||||
|
@ -251,7 +310,8 @@ impl TestAppContext {
|
||||||
|
|
||||||
/// simulate_keystrokes takes a space-separated list of keys to type.
|
/// simulate_keystrokes takes a space-separated list of keys to type.
|
||||||
/// cx.simulate_keystrokes("cmd-shift-p b k s p enter")
|
/// cx.simulate_keystrokes("cmd-shift-p b k s p enter")
|
||||||
/// will run backspace on the current editor through the command palette.
|
/// in Zed, this will run backspace on the current editor through the command palette.
|
||||||
|
/// This will also run the background executor until it's parked.
|
||||||
pub fn simulate_keystrokes(&mut self, window: AnyWindowHandle, keystrokes: &str) {
|
pub fn simulate_keystrokes(&mut self, window: AnyWindowHandle, keystrokes: &str) {
|
||||||
for keystroke in keystrokes
|
for keystroke in keystrokes
|
||||||
.split(" ")
|
.split(" ")
|
||||||
|
@ -266,7 +326,8 @@ impl TestAppContext {
|
||||||
|
|
||||||
/// simulate_input takes a string of text to type.
|
/// simulate_input takes a string of text to type.
|
||||||
/// cx.simulate_input("abc")
|
/// cx.simulate_input("abc")
|
||||||
/// will type abc into your current editor.
|
/// will type abc into your current editor
|
||||||
|
/// This will also run the background executor until it's parked.
|
||||||
pub fn simulate_input(&mut self, window: AnyWindowHandle, input: &str) {
|
pub fn simulate_input(&mut self, window: AnyWindowHandle, input: &str) {
|
||||||
for keystroke in input.split("").map(Keystroke::parse).map(Result::unwrap) {
|
for keystroke in input.split("").map(Keystroke::parse).map(Result::unwrap) {
|
||||||
self.dispatch_keystroke(window, keystroke.into(), false);
|
self.dispatch_keystroke(window, keystroke.into(), false);
|
||||||
|
@ -275,6 +336,7 @@ impl TestAppContext {
|
||||||
self.background_executor.run_until_parked()
|
self.background_executor.run_until_parked()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// dispatches a single Keystroke (see also `simulate_keystrokes` and `simulate_input`)
|
||||||
pub fn dispatch_keystroke(
|
pub fn dispatch_keystroke(
|
||||||
&mut self,
|
&mut self,
|
||||||
window: AnyWindowHandle,
|
window: AnyWindowHandle,
|
||||||
|
@ -285,6 +347,7 @@ impl TestAppContext {
|
||||||
.simulate_keystroke(keystroke, is_held)
|
.simulate_keystroke(keystroke, is_held)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the `TestWindow` backing the given handle.
|
||||||
pub fn test_window(&self, window: AnyWindowHandle) -> TestWindow {
|
pub fn test_window(&self, window: AnyWindowHandle) -> TestWindow {
|
||||||
self.app
|
self.app
|
||||||
.borrow_mut()
|
.borrow_mut()
|
||||||
|
@ -299,6 +362,7 @@ impl TestAppContext {
|
||||||
.clone()
|
.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns a stream of notifications whenever the View or Model is updated.
|
||||||
pub fn notifications<T: 'static>(&mut self, entity: &impl Entity<T>) -> impl Stream<Item = ()> {
|
pub fn notifications<T: 'static>(&mut self, entity: &impl Entity<T>) -> impl Stream<Item = ()> {
|
||||||
let (tx, rx) = futures::channel::mpsc::unbounded();
|
let (tx, rx) = futures::channel::mpsc::unbounded();
|
||||||
self.update(|cx| {
|
self.update(|cx| {
|
||||||
|
@ -315,6 +379,7 @@ impl TestAppContext {
|
||||||
rx
|
rx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Retuens a stream of events emitted by the given Model.
|
||||||
pub fn events<Evt, T: 'static + EventEmitter<Evt>>(
|
pub fn events<Evt, T: 'static + EventEmitter<Evt>>(
|
||||||
&mut self,
|
&mut self,
|
||||||
entity: &Model<T>,
|
entity: &Model<T>,
|
||||||
|
@ -333,6 +398,8 @@ impl TestAppContext {
|
||||||
rx
|
rx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Runs until the given condition becomes true. (Prefer `run_until_parked` if you
|
||||||
|
/// don't need to jump in at a specific time).
|
||||||
pub async fn condition<T: 'static>(
|
pub async fn condition<T: 'static>(
|
||||||
&mut self,
|
&mut self,
|
||||||
model: &Model<T>,
|
model: &Model<T>,
|
||||||
|
@ -362,6 +429,7 @@ impl TestAppContext {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Send> Model<T> {
|
impl<T: Send> Model<T> {
|
||||||
|
/// Block until the next event is emitted by the model, then return it.
|
||||||
pub fn next_event<Evt>(&self, cx: &mut TestAppContext) -> Evt
|
pub fn next_event<Evt>(&self, cx: &mut TestAppContext) -> Evt
|
||||||
where
|
where
|
||||||
Evt: Send + Clone + 'static,
|
Evt: Send + Clone + 'static,
|
||||||
|
@ -391,6 +459,7 @@ impl<T: Send> Model<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<V: 'static> View<V> {
|
impl<V: 'static> View<V> {
|
||||||
|
/// Returns a future that resolves when the view is next updated.
|
||||||
pub fn next_notification(&self, cx: &TestAppContext) -> impl Future<Output = ()> {
|
pub fn next_notification(&self, cx: &TestAppContext) -> impl Future<Output = ()> {
|
||||||
use postage::prelude::{Sink as _, Stream as _};
|
use postage::prelude::{Sink as _, Stream as _};
|
||||||
|
|
||||||
|
@ -417,6 +486,7 @@ impl<V: 'static> View<V> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<V> View<V> {
|
impl<V> View<V> {
|
||||||
|
/// Returns a future that resolves when the condition becomes true.
|
||||||
pub fn condition<Evt>(
|
pub fn condition<Evt>(
|
||||||
&self,
|
&self,
|
||||||
cx: &TestAppContext,
|
cx: &TestAppContext,
|
||||||
|
@ -429,7 +499,7 @@ impl<V> View<V> {
|
||||||
use postage::prelude::{Sink as _, Stream as _};
|
use postage::prelude::{Sink as _, Stream as _};
|
||||||
|
|
||||||
let (tx, mut rx) = postage::mpsc::channel(1024);
|
let (tx, mut rx) = postage::mpsc::channel(1024);
|
||||||
let timeout_duration = Duration::from_millis(100); //todo!() cx.condition_duration();
|
let timeout_duration = Duration::from_millis(100);
|
||||||
|
|
||||||
let mut cx = cx.app.borrow_mut();
|
let mut cx = cx.app.borrow_mut();
|
||||||
let subscriptions = (
|
let subscriptions = (
|
||||||
|
@ -467,12 +537,11 @@ impl<V> View<V> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// todo!(start_waiting)
|
cx.borrow().background_executor().start_waiting();
|
||||||
// cx.borrow().foreground_executor().start_waiting();
|
|
||||||
rx.recv()
|
rx.recv()
|
||||||
.await
|
.await
|
||||||
.expect("view dropped with pending condition");
|
.expect("view dropped with pending condition");
|
||||||
// cx.borrow().foreground_executor().finish_waiting();
|
cx.borrow().background_executor().finish_waiting();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
|
@ -484,18 +553,25 @@ impl<V> View<V> {
|
||||||
|
|
||||||
use derive_more::{Deref, DerefMut};
|
use derive_more::{Deref, DerefMut};
|
||||||
#[derive(Deref, DerefMut, Clone)]
|
#[derive(Deref, DerefMut, Clone)]
|
||||||
|
/// A VisualTestContext is the test-equivalent of a `WindowContext`. It allows you to
|
||||||
|
/// run window-specific test code.
|
||||||
pub struct VisualTestContext {
|
pub struct VisualTestContext {
|
||||||
#[deref]
|
#[deref]
|
||||||
#[deref_mut]
|
#[deref_mut]
|
||||||
cx: TestAppContext,
|
/// cx is the original TestAppContext (you can more easily access this using Deref)
|
||||||
|
pub cx: TestAppContext,
|
||||||
window: AnyWindowHandle,
|
window: AnyWindowHandle,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> VisualTestContext {
|
impl<'a> VisualTestContext {
|
||||||
|
/// Provides the `WindowContext` for the duration of the closure.
|
||||||
pub fn update<R>(&mut self, f: impl FnOnce(&mut WindowContext) -> R) -> R {
|
pub fn update<R>(&mut self, f: impl FnOnce(&mut WindowContext) -> R) -> R {
|
||||||
self.cx.update_window(self.window, |_, cx| f(cx)).unwrap()
|
self.cx.update_window(self.window, |_, cx| f(cx)).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a new VisualTestContext. You would typically shadow the passed in
|
||||||
|
/// TestAppContext with this, as this is typically more useful.
|
||||||
|
/// `let cx = VisualTestContext::from_window(window, cx);`
|
||||||
pub fn from_window(window: AnyWindowHandle, cx: &TestAppContext) -> Self {
|
pub fn from_window(window: AnyWindowHandle, cx: &TestAppContext) -> Self {
|
||||||
Self {
|
Self {
|
||||||
cx: cx.clone(),
|
cx: cx.clone(),
|
||||||
|
@ -503,10 +579,12 @@ impl<'a> VisualTestContext {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Wait until there are no more pending tasks.
|
||||||
pub fn run_until_parked(&self) {
|
pub fn run_until_parked(&self) {
|
||||||
self.cx.background_executor.run_until_parked();
|
self.cx.background_executor.run_until_parked();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Dispatch the action to the currently focused node.
|
||||||
pub fn dispatch_action<A>(&mut self, action: A)
|
pub fn dispatch_action<A>(&mut self, action: A)
|
||||||
where
|
where
|
||||||
A: Action,
|
A: Action,
|
||||||
|
@ -514,24 +592,32 @@ impl<'a> VisualTestContext {
|
||||||
self.cx.dispatch_action(self.window, action)
|
self.cx.dispatch_action(self.window, action)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Read the title off the window (set by `WindowContext#set_window_title`)
|
||||||
pub fn window_title(&mut self) -> Option<String> {
|
pub fn window_title(&mut self) -> Option<String> {
|
||||||
self.cx.test_window(self.window).0.lock().title.clone()
|
self.cx.test_window(self.window).0.lock().title.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Simulate a sequence of keystrokes `cx.simulate_keystrokes("cmd-p escape")`
|
||||||
|
/// Automatically runs until parked.
|
||||||
pub fn simulate_keystrokes(&mut self, keystrokes: &str) {
|
pub fn simulate_keystrokes(&mut self, keystrokes: &str) {
|
||||||
self.cx.simulate_keystrokes(self.window, keystrokes)
|
self.cx.simulate_keystrokes(self.window, keystrokes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Simulate typing text `cx.simulate_input("hello")`
|
||||||
|
/// Automatically runs until parked.
|
||||||
pub fn simulate_input(&mut self, input: &str) {
|
pub fn simulate_input(&mut self, input: &str) {
|
||||||
self.cx.simulate_input(self.window, input)
|
self.cx.simulate_input(self.window, input)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Simulates the user blurring the window.
|
||||||
pub fn deactivate_window(&mut self) {
|
pub fn deactivate_window(&mut self) {
|
||||||
if Some(self.window) == self.test_platform.active_window() {
|
if Some(self.window) == self.test_platform.active_window() {
|
||||||
self.test_platform.set_active_window(None)
|
self.test_platform.set_active_window(None)
|
||||||
}
|
}
|
||||||
self.background_executor.run_until_parked();
|
self.background_executor.run_until_parked();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Simulates the user closing the window.
|
||||||
/// Returns true if the window was closed.
|
/// Returns true if the window was closed.
|
||||||
pub fn simulate_close(&mut self) -> bool {
|
pub fn simulate_close(&mut self) -> bool {
|
||||||
let handler = self
|
let handler = self
|
||||||
|
@ -668,6 +754,7 @@ impl VisualContext for VisualTestContext {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AnyWindowHandle {
|
impl AnyWindowHandle {
|
||||||
|
/// Creates the given view in this window.
|
||||||
pub fn build_view<V: Render + 'static>(
|
pub fn build_view<V: Render + 'static>(
|
||||||
&self,
|
&self,
|
||||||
cx: &mut TestAppContext,
|
cx: &mut TestAppContext,
|
||||||
|
@ -677,6 +764,7 @@ impl AnyWindowHandle {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// An EmptyView for testing.
|
||||||
pub struct EmptyView {}
|
pub struct EmptyView {}
|
||||||
|
|
||||||
impl Render for EmptyView {
|
impl Render for EmptyView {
|
||||||
|
|
|
@ -66,18 +66,19 @@ impl Arena {
|
||||||
}
|
}
|
||||||
|
|
||||||
unsafe {
|
unsafe {
|
||||||
let layout = alloc::Layout::new::<T>().pad_to_align();
|
let layout = alloc::Layout::new::<T>();
|
||||||
let next_offset = self.offset.add(layout.size());
|
let offset = self.offset.add(self.offset.align_offset(layout.align()));
|
||||||
assert!(next_offset <= self.end);
|
let next_offset = offset.add(layout.size());
|
||||||
|
assert!(next_offset <= self.end, "not enough space in Arena");
|
||||||
|
|
||||||
let result = ArenaBox {
|
let result = ArenaBox {
|
||||||
ptr: self.offset.cast(),
|
ptr: offset.cast(),
|
||||||
valid: self.valid.clone(),
|
valid: self.valid.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
inner_writer(result.ptr, f);
|
inner_writer(result.ptr, f);
|
||||||
self.elements.push(ArenaElement {
|
self.elements.push(ArenaElement {
|
||||||
value: self.offset,
|
value: offset,
|
||||||
drop: drop::<T>,
|
drop: drop::<T>,
|
||||||
});
|
});
|
||||||
self.offset = next_offset;
|
self.offset = next_offset;
|
||||||
|
@ -199,4 +200,43 @@ mod tests {
|
||||||
arena.clear();
|
arena.clear();
|
||||||
assert!(dropped.get());
|
assert!(dropped.get());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic(expected = "not enough space in Arena")]
|
||||||
|
fn test_arena_overflow() {
|
||||||
|
let mut arena = Arena::new(16);
|
||||||
|
arena.alloc(|| 1u64);
|
||||||
|
arena.alloc(|| 2u64);
|
||||||
|
// This should panic.
|
||||||
|
arena.alloc(|| 3u64);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_arena_alignment() {
|
||||||
|
let mut arena = Arena::new(256);
|
||||||
|
let x1 = arena.alloc(|| 1u8);
|
||||||
|
let x2 = arena.alloc(|| 2u16);
|
||||||
|
let x3 = arena.alloc(|| 3u32);
|
||||||
|
let x4 = arena.alloc(|| 4u64);
|
||||||
|
let x5 = arena.alloc(|| 5u64);
|
||||||
|
|
||||||
|
assert_eq!(*x1, 1);
|
||||||
|
assert_eq!(*x2, 2);
|
||||||
|
assert_eq!(*x3, 3);
|
||||||
|
assert_eq!(*x4, 4);
|
||||||
|
assert_eq!(*x5, 5);
|
||||||
|
|
||||||
|
assert_eq!(x1.ptr.align_offset(std::mem::align_of_val(&*x1)), 0);
|
||||||
|
assert_eq!(x2.ptr.align_offset(std::mem::align_of_val(&*x2)), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic(expected = "attempted to dereference an ArenaRef after its Arena was cleared")]
|
||||||
|
fn test_arena_use_after_clear() {
|
||||||
|
let mut arena = Arena::new(16);
|
||||||
|
let value = arena.alloc(|| 1u64);
|
||||||
|
|
||||||
|
arena.clear();
|
||||||
|
let _read_value = *value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -321,7 +321,7 @@ impl Hsla {
|
||||||
///
|
///
|
||||||
/// Assumptions:
|
/// Assumptions:
|
||||||
/// - Alpha values are contained in the range [0, 1], with 1 as fully opaque and 0 as fully transparent.
|
/// - Alpha values are contained in the range [0, 1], with 1 as fully opaque and 0 as fully transparent.
|
||||||
/// - The relative contributions of `self` and `other` is based on `self`'s alpha value (`self.a`) and `other`'s alpha value (`other.a`), `self` contributing `self.a * (1.0 - other.a)` and `other` contributing it's own alpha value.
|
/// - The relative contributions of `self` and `other` is based on `self`'s alpha value (`self.a`) and `other`'s alpha value (`other.a`), `self` contributing `self.a * (1.0 - other.a)` and `other` contributing its own alpha value.
|
||||||
/// - RGB color components are contained in the range [0, 1].
|
/// - RGB color components are contained in the range [0, 1].
|
||||||
/// - If `self` and `other` colors are out of the valid range, the blend operation's output and behavior is undefined.
|
/// - If `self` and `other` colors are out of the valid range, the blend operation's output and behavior is undefined.
|
||||||
pub fn blend(self, other: Hsla) -> Hsla {
|
pub fn blend(self, other: Hsla) -> Hsla {
|
||||||
|
|
|
@ -31,14 +31,14 @@ pub trait IntoElement: Sized {
|
||||||
/// The specific type of element into which the implementing type is converted.
|
/// The specific type of element into which the implementing type is converted.
|
||||||
type Element: Element;
|
type Element: Element;
|
||||||
|
|
||||||
/// The [ElementId] of self once converted into an [Element].
|
/// The [`ElementId`] of self once converted into an [`Element`].
|
||||||
/// If present, the resulting element's state will be carried across frames.
|
/// If present, the resulting element's state will be carried across frames.
|
||||||
fn element_id(&self) -> Option<ElementId>;
|
fn element_id(&self) -> Option<ElementId>;
|
||||||
|
|
||||||
/// Convert self into a type that implements [Element].
|
/// Convert self into a type that implements [`Element`].
|
||||||
fn into_element(self) -> Self::Element;
|
fn into_element(self) -> Self::Element;
|
||||||
|
|
||||||
/// Convert self into a dynamically-typed [AnyElement].
|
/// Convert self into a dynamically-typed [`AnyElement`].
|
||||||
fn into_any_element(self) -> AnyElement {
|
fn into_any_element(self) -> AnyElement {
|
||||||
self.into_element().into_any()
|
self.into_element().into_any()
|
||||||
}
|
}
|
||||||
|
@ -115,7 +115,7 @@ pub trait Render: 'static + Sized {
|
||||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement;
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// You can derive [IntoElement] on any type that implements this trait.
|
/// You can derive [`IntoElement`] on any type that implements this trait.
|
||||||
/// It is used to allow views to be expressed in terms of abstract data.
|
/// It is used to allow views to be expressed in terms of abstract data.
|
||||||
pub trait RenderOnce: 'static {
|
pub trait RenderOnce: 'static {
|
||||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement;
|
fn render(self, cx: &mut WindowContext) -> impl IntoElement;
|
||||||
|
@ -224,7 +224,7 @@ enum ElementDrawPhase<S> {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A wrapper around an implementer of [Element] that allows it to be drawn in a window.
|
/// A wrapper around an implementer of [`Element`] that allows it to be drawn in a window.
|
||||||
impl<E: Element> DrawableElement<E> {
|
impl<E: Element> DrawableElement<E> {
|
||||||
fn new(element: E) -> Self {
|
fn new(element: E) -> Self {
|
||||||
DrawableElement {
|
DrawableElement {
|
||||||
|
|
|
@ -1003,7 +1003,7 @@ impl Interactivity {
|
||||||
if let Some(text) = cx
|
if let Some(text) = cx
|
||||||
.text_system()
|
.text_system()
|
||||||
.shape_text(
|
.shape_text(
|
||||||
&element_id,
|
element_id.into(),
|
||||||
FONT_SIZE,
|
FONT_SIZE,
|
||||||
&[cx.text_style().to_run(str_len)],
|
&[cx.text_style().to_run(str_len)],
|
||||||
None,
|
None,
|
||||||
|
@ -1055,22 +1055,11 @@ impl Interactivity {
|
||||||
};
|
};
|
||||||
|
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"This element is created at:\n{}:{}:{}",
|
"This element was created at:\n{}:{}:{}",
|
||||||
location.file(),
|
dir.join(location.file()).to_string_lossy(),
|
||||||
location.line(),
|
location.line(),
|
||||||
location.column()
|
location.column()
|
||||||
);
|
);
|
||||||
|
|
||||||
std::process::Command::new("zed")
|
|
||||||
.arg(format!(
|
|
||||||
"{}/{}:{}:{}",
|
|
||||||
dir.to_string_lossy(),
|
|
||||||
location.file(),
|
|
||||||
location.line(),
|
|
||||||
location.column()
|
|
||||||
))
|
|
||||||
.spawn()
|
|
||||||
.ok();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -2,7 +2,7 @@ use std::sync::Arc;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
point, size, BorrowWindow, Bounds, DevicePixels, Element, ImageData, InteractiveElement,
|
point, size, BorrowWindow, Bounds, DevicePixels, Element, ImageData, InteractiveElement,
|
||||||
InteractiveElementState, Interactivity, IntoElement, LayoutId, Pixels, SharedString, Size,
|
InteractiveElementState, Interactivity, IntoElement, LayoutId, Pixels, SharedUrl, Size,
|
||||||
StyleRefinement, Styled, WindowContext,
|
StyleRefinement, Styled, WindowContext,
|
||||||
};
|
};
|
||||||
use futures::FutureExt;
|
use futures::FutureExt;
|
||||||
|
@ -12,13 +12,13 @@ use util::ResultExt;
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum ImageSource {
|
pub enum ImageSource {
|
||||||
/// Image content will be loaded from provided URI at render time.
|
/// Image content will be loaded from provided URI at render time.
|
||||||
Uri(SharedString),
|
Uri(SharedUrl),
|
||||||
Data(Arc<ImageData>),
|
Data(Arc<ImageData>),
|
||||||
Surface(CVImageBuffer),
|
Surface(CVImageBuffer),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<SharedString> for ImageSource {
|
impl From<SharedUrl> for ImageSource {
|
||||||
fn from(value: SharedString) -> Self {
|
fn from(value: SharedUrl) -> Self {
|
||||||
Self::Uri(value)
|
Self::Uri(value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,9 +14,8 @@ pub struct Overlay {
|
||||||
children: SmallVec<[AnyElement; 2]>,
|
children: SmallVec<[AnyElement; 2]>,
|
||||||
anchor_corner: AnchorCorner,
|
anchor_corner: AnchorCorner,
|
||||||
fit_mode: OverlayFitMode,
|
fit_mode: OverlayFitMode,
|
||||||
// todo!();
|
|
||||||
anchor_position: Option<Point<Pixels>>,
|
anchor_position: Option<Point<Pixels>>,
|
||||||
// position_mode: OverlayPositionMode,
|
position_mode: OverlayPositionMode,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// overlay gives you a floating element that will avoid overflowing the window bounds.
|
/// overlay gives you a floating element that will avoid overflowing the window bounds.
|
||||||
|
@ -27,6 +26,7 @@ pub fn overlay() -> Overlay {
|
||||||
anchor_corner: AnchorCorner::TopLeft,
|
anchor_corner: AnchorCorner::TopLeft,
|
||||||
fit_mode: OverlayFitMode::SwitchAnchor,
|
fit_mode: OverlayFitMode::SwitchAnchor,
|
||||||
anchor_position: None,
|
anchor_position: None,
|
||||||
|
position_mode: OverlayPositionMode::Window,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,6 +44,14 @@ impl Overlay {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets the position mode for this overlay. Local will have this
|
||||||
|
/// interpret its [`Overlay::position`] as relative to the parent element.
|
||||||
|
/// While Window will have it interpret the position as relative to the window.
|
||||||
|
pub fn position_mode(mut self, mode: OverlayPositionMode) -> Self {
|
||||||
|
self.position_mode = mode;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Snap to window edge instead of switching anchor corner when an overflow would occur.
|
/// Snap to window edge instead of switching anchor corner when an overflow would occur.
|
||||||
pub fn snap_to_window(mut self) -> Self {
|
pub fn snap_to_window(mut self) -> Self {
|
||||||
self.fit_mode = OverlayFitMode::SnapToWindow;
|
self.fit_mode = OverlayFitMode::SnapToWindow;
|
||||||
|
@ -100,9 +108,14 @@ impl Element for Overlay {
|
||||||
child_max = child_max.max(&child_bounds.lower_right());
|
child_max = child_max.max(&child_bounds.lower_right());
|
||||||
}
|
}
|
||||||
let size: Size<Pixels> = (child_max - child_min).into();
|
let size: Size<Pixels> = (child_max - child_min).into();
|
||||||
let origin = self.anchor_position.unwrap_or(bounds.origin);
|
|
||||||
|
|
||||||
let mut desired = self.anchor_corner.get_bounds(origin, size);
|
let (origin, mut desired) = self.position_mode.get_position_and_bounds(
|
||||||
|
self.anchor_position,
|
||||||
|
self.anchor_corner,
|
||||||
|
size,
|
||||||
|
bounds,
|
||||||
|
);
|
||||||
|
|
||||||
let limits = Bounds {
|
let limits = Bounds {
|
||||||
origin: Point::default(),
|
origin: Point::default(),
|
||||||
size: cx.viewport_size(),
|
size: cx.viewport_size(),
|
||||||
|
@ -184,6 +197,35 @@ pub enum OverlayFitMode {
|
||||||
SwitchAnchor,
|
SwitchAnchor,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, PartialEq)]
|
||||||
|
pub enum OverlayPositionMode {
|
||||||
|
Window,
|
||||||
|
Local,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OverlayPositionMode {
|
||||||
|
fn get_position_and_bounds(
|
||||||
|
&self,
|
||||||
|
anchor_position: Option<Point<Pixels>>,
|
||||||
|
anchor_corner: AnchorCorner,
|
||||||
|
size: Size<Pixels>,
|
||||||
|
bounds: Bounds<Pixels>,
|
||||||
|
) -> (Point<Pixels>, Bounds<Pixels>) {
|
||||||
|
match self {
|
||||||
|
OverlayPositionMode::Window => {
|
||||||
|
let anchor_position = anchor_position.unwrap_or_else(|| bounds.origin);
|
||||||
|
let bounds = anchor_corner.get_bounds(anchor_position, size);
|
||||||
|
(anchor_position, bounds)
|
||||||
|
}
|
||||||
|
OverlayPositionMode::Local => {
|
||||||
|
let anchor_position = anchor_position.unwrap_or_default();
|
||||||
|
let bounds = anchor_corner.get_bounds(bounds.origin + anchor_position, size);
|
||||||
|
(anchor_position, bounds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum AnchorCorner {
|
pub enum AnchorCorner {
|
||||||
TopLeft,
|
TopLeft,
|
||||||
|
|
|
@ -202,7 +202,10 @@ impl TextState {
|
||||||
let Some(lines) = cx
|
let Some(lines) = cx
|
||||||
.text_system()
|
.text_system()
|
||||||
.shape_text(
|
.shape_text(
|
||||||
&text, font_size, &runs, wrap_width, // Wrap if we know the width.
|
text.clone(),
|
||||||
|
font_size,
|
||||||
|
&runs,
|
||||||
|
wrap_width, // Wrap if we know the width.
|
||||||
)
|
)
|
||||||
.log_err()
|
.log_err()
|
||||||
else {
|
else {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
point, px, size, AnyElement, AvailableSpace, BorrowWindow, Bounds, ContentMask, Element,
|
point, px, size, AnyElement, AvailableSpace, BorrowWindow, Bounds, ContentMask, Element,
|
||||||
ElementId, InteractiveElement, InteractiveElementState, Interactivity, IntoElement, LayoutId,
|
ElementId, InteractiveElement, InteractiveElementState, Interactivity, IntoElement, LayoutId,
|
||||||
Pixels, Point, Render, Size, StyleRefinement, Styled, View, ViewContext, WindowContext,
|
Pixels, Render, Size, StyleRefinement, Styled, View, ViewContext, WindowContext,
|
||||||
};
|
};
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
use std::{cell::RefCell, cmp, ops::Range, rc::Rc};
|
use std::{cell::RefCell, cmp, ops::Range, rc::Rc};
|
||||||
|
@ -64,40 +64,19 @@ pub struct UniformList {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Default)]
|
#[derive(Clone, Default)]
|
||||||
pub struct UniformListScrollHandle(Rc<RefCell<Option<ScrollHandleState>>>);
|
pub struct UniformListScrollHandle {
|
||||||
|
deferred_scroll_to_item: Rc<RefCell<Option<usize>>>,
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
struct ScrollHandleState {
|
|
||||||
item_height: Pixels,
|
|
||||||
list_height: Pixels,
|
|
||||||
scroll_offset: Rc<RefCell<Point<Pixels>>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UniformListScrollHandle {
|
impl UniformListScrollHandle {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self(Rc::new(RefCell::new(None)))
|
Self {
|
||||||
}
|
deferred_scroll_to_item: Rc::new(RefCell::new(None)),
|
||||||
|
|
||||||
pub fn scroll_to_item(&self, ix: usize) {
|
|
||||||
if let Some(state) = &*self.0.borrow() {
|
|
||||||
let mut scroll_offset = state.scroll_offset.borrow_mut();
|
|
||||||
let item_top = state.item_height * ix;
|
|
||||||
let item_bottom = item_top + state.item_height;
|
|
||||||
let scroll_top = -scroll_offset.y;
|
|
||||||
if item_top < scroll_top {
|
|
||||||
scroll_offset.y = -item_top;
|
|
||||||
} else if item_bottom > scroll_top + state.list_height {
|
|
||||||
scroll_offset.y = -(item_bottom - state.list_height);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn scroll_top(&self) -> Pixels {
|
pub fn scroll_to_item(&mut self, ix: usize) {
|
||||||
if let Some(state) = &*self.0.borrow() {
|
self.deferred_scroll_to_item.replace(Some(ix));
|
||||||
-state.scroll_offset.borrow().y
|
|
||||||
} else {
|
|
||||||
Pixels::ZERO
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -190,18 +169,14 @@ impl Element for UniformList {
|
||||||
let shared_scroll_offset = element_state
|
let shared_scroll_offset = element_state
|
||||||
.interactive
|
.interactive
|
||||||
.scroll_offset
|
.scroll_offset
|
||||||
.get_or_insert_with(|| {
|
.get_or_insert_with(|| Rc::default())
|
||||||
if let Some(scroll_handle) = self.scroll_handle.as_ref() {
|
|
||||||
if let Some(scroll_handle) = scroll_handle.0.borrow().as_ref() {
|
|
||||||
return scroll_handle.scroll_offset.clone();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rc::default()
|
|
||||||
})
|
|
||||||
.clone();
|
.clone();
|
||||||
|
|
||||||
let item_height = self.measure_item(Some(padded_bounds.size.width), cx).height;
|
let item_height = self.measure_item(Some(padded_bounds.size.width), cx).height;
|
||||||
|
let shared_scroll_to_item = self
|
||||||
|
.scroll_handle
|
||||||
|
.as_mut()
|
||||||
|
.and_then(|handle| handle.deferred_scroll_to_item.take());
|
||||||
|
|
||||||
self.interactivity.paint(
|
self.interactivity.paint(
|
||||||
bounds,
|
bounds,
|
||||||
|
@ -228,12 +203,18 @@ impl Element for UniformList {
|
||||||
scroll_offset.y = min_scroll_offset;
|
scroll_offset.y = min_scroll_offset;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(scroll_handle) = self.scroll_handle.clone() {
|
if let Some(ix) = shared_scroll_to_item {
|
||||||
scroll_handle.0.borrow_mut().replace(ScrollHandleState {
|
let list_height = padded_bounds.size.height;
|
||||||
item_height,
|
let mut updated_scroll_offset = shared_scroll_offset.borrow_mut();
|
||||||
list_height: padded_bounds.size.height,
|
let item_top = item_height * ix;
|
||||||
scroll_offset: shared_scroll_offset,
|
let item_bottom = item_top + item_height;
|
||||||
});
|
let scroll_top = -updated_scroll_offset.y;
|
||||||
|
if item_top < scroll_top {
|
||||||
|
updated_scroll_offset.y = -item_top;
|
||||||
|
} else if item_bottom > scroll_top + list_height {
|
||||||
|
updated_scroll_offset.y = -(item_bottom - list_height);
|
||||||
|
}
|
||||||
|
scroll_offset = *updated_scroll_offset;
|
||||||
}
|
}
|
||||||
|
|
||||||
let first_visible_element_ix =
|
let first_visible_element_ix =
|
||||||
|
|
|
@ -32,6 +32,12 @@ pub struct ForegroundExecutor {
|
||||||
not_send: PhantomData<Rc<()>>,
|
not_send: PhantomData<Rc<()>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Task is a primitive that allows work to happen in the background.
|
||||||
|
///
|
||||||
|
/// It implements [`Future`] so you can `.await` on it.
|
||||||
|
///
|
||||||
|
/// If you drop a task it will be cancelled immediately. Calling [`Task::detach`] allows
|
||||||
|
/// the task to continue running in the background, but with no way to return a value.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Task<T> {
|
pub enum Task<T> {
|
||||||
|
@ -40,10 +46,12 @@ pub enum Task<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> Task<T> {
|
impl<T> Task<T> {
|
||||||
|
/// Create a new task that will resolve with the value
|
||||||
pub fn ready(val: T) -> Self {
|
pub fn ready(val: T) -> Self {
|
||||||
Task::Ready(Some(val))
|
Task::Ready(Some(val))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Detaching a task runs it to completion in the background
|
||||||
pub fn detach(self) {
|
pub fn detach(self) {
|
||||||
match self {
|
match self {
|
||||||
Task::Ready(_) => {}
|
Task::Ready(_) => {}
|
||||||
|
@ -57,6 +65,8 @@ where
|
||||||
T: 'static,
|
T: 'static,
|
||||||
E: 'static + Debug,
|
E: 'static + Debug,
|
||||||
{
|
{
|
||||||
|
/// Run the task to completion in the background and log any
|
||||||
|
/// errors that occur.
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
pub fn detach_and_log_err(self, cx: &mut AppContext) {
|
pub fn detach_and_log_err(self, cx: &mut AppContext) {
|
||||||
let location = core::panic::Location::caller();
|
let location = core::panic::Location::caller();
|
||||||
|
@ -97,6 +107,10 @@ type AnyLocalFuture<R> = Pin<Box<dyn 'static + Future<Output = R>>>;
|
||||||
|
|
||||||
type AnyFuture<R> = Pin<Box<dyn 'static + Send + Future<Output = R>>>;
|
type AnyFuture<R> = Pin<Box<dyn 'static + Send + Future<Output = R>>>;
|
||||||
|
|
||||||
|
/// BackgroundExecutor lets you run things on background threads.
|
||||||
|
/// In production this is a thread pool with no ordering guarantees.
|
||||||
|
/// In tests this is simalated by running tasks one by one in a deterministic
|
||||||
|
/// (but arbitrary) order controlled by the `SEED` environment variable.
|
||||||
impl BackgroundExecutor {
|
impl BackgroundExecutor {
|
||||||
pub fn new(dispatcher: Arc<dyn PlatformDispatcher>) -> Self {
|
pub fn new(dispatcher: Arc<dyn PlatformDispatcher>) -> Self {
|
||||||
Self { dispatcher }
|
Self { dispatcher }
|
||||||
|
@ -135,6 +149,7 @@ impl BackgroundExecutor {
|
||||||
Task::Spawned(task)
|
Task::Spawned(task)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Used by the test harness to run an async test in a syncronous fashion.
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
pub fn block_test<R>(&self, future: impl Future<Output = R>) -> R {
|
pub fn block_test<R>(&self, future: impl Future<Output = R>) -> R {
|
||||||
|
@ -145,6 +160,8 @@ impl BackgroundExecutor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Block the current thread until the given future resolves.
|
||||||
|
/// Consider using `block_with_timeout` instead.
|
||||||
pub fn block<R>(&self, future: impl Future<Output = R>) -> R {
|
pub fn block<R>(&self, future: impl Future<Output = R>) -> R {
|
||||||
if let Ok(value) = self.block_internal(true, future, usize::MAX) {
|
if let Ok(value) = self.block_internal(true, future, usize::MAX) {
|
||||||
value
|
value
|
||||||
|
@ -206,6 +223,8 @@ impl BackgroundExecutor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Block the current thread until the given future resolves
|
||||||
|
/// or `duration` has elapsed.
|
||||||
pub fn block_with_timeout<R>(
|
pub fn block_with_timeout<R>(
|
||||||
&self,
|
&self,
|
||||||
duration: Duration,
|
duration: Duration,
|
||||||
|
@ -238,6 +257,8 @@ impl BackgroundExecutor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Scoped lets you start a number of tasks and waits
|
||||||
|
/// for all of them to complete before returning.
|
||||||
pub async fn scoped<'scope, F>(&self, scheduler: F)
|
pub async fn scoped<'scope, F>(&self, scheduler: F)
|
||||||
where
|
where
|
||||||
F: FnOnce(&mut Scope<'scope>),
|
F: FnOnce(&mut Scope<'scope>),
|
||||||
|
@ -253,6 +274,9 @@ impl BackgroundExecutor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns a task that will complete after the given duration.
|
||||||
|
/// Depending on other concurrent tasks the elapsed duration may be longer
|
||||||
|
/// than reqested.
|
||||||
pub fn timer(&self, duration: Duration) -> Task<()> {
|
pub fn timer(&self, duration: Duration) -> Task<()> {
|
||||||
let (runnable, task) = async_task::spawn(async move {}, {
|
let (runnable, task) = async_task::spawn(async move {}, {
|
||||||
let dispatcher = self.dispatcher.clone();
|
let dispatcher = self.dispatcher.clone();
|
||||||
|
@ -262,65 +286,81 @@ impl BackgroundExecutor {
|
||||||
Task::Spawned(task)
|
Task::Spawned(task)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// in tests, start_waiting lets you indicate which task is waiting (for debugging only)
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
pub fn start_waiting(&self) {
|
pub fn start_waiting(&self) {
|
||||||
self.dispatcher.as_test().unwrap().start_waiting();
|
self.dispatcher.as_test().unwrap().start_waiting();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// in tests, removes the debugging data added by start_waiting
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
pub fn finish_waiting(&self) {
|
pub fn finish_waiting(&self) {
|
||||||
self.dispatcher.as_test().unwrap().finish_waiting();
|
self.dispatcher.as_test().unwrap().finish_waiting();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// in tests, run an arbitrary number of tasks (determined by the SEED environment variable)
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
pub fn simulate_random_delay(&self) -> impl Future<Output = ()> {
|
pub fn simulate_random_delay(&self) -> impl Future<Output = ()> {
|
||||||
self.dispatcher.as_test().unwrap().simulate_random_delay()
|
self.dispatcher.as_test().unwrap().simulate_random_delay()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// in tests, indicate that a given task from `spawn_labeled` should run after everything else
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
pub fn deprioritize(&self, task_label: TaskLabel) {
|
pub fn deprioritize(&self, task_label: TaskLabel) {
|
||||||
self.dispatcher.as_test().unwrap().deprioritize(task_label)
|
self.dispatcher.as_test().unwrap().deprioritize(task_label)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// in tests, move time forward. This does not run any tasks, but does make `timer`s ready.
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
pub fn advance_clock(&self, duration: Duration) {
|
pub fn advance_clock(&self, duration: Duration) {
|
||||||
self.dispatcher.as_test().unwrap().advance_clock(duration)
|
self.dispatcher.as_test().unwrap().advance_clock(duration)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// in tests, run one task.
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
pub fn tick(&self) -> bool {
|
pub fn tick(&self) -> bool {
|
||||||
self.dispatcher.as_test().unwrap().tick(false)
|
self.dispatcher.as_test().unwrap().tick(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// in tests, run all tasks that are ready to run. If after doing so
|
||||||
|
/// the test still has outstanding tasks, this will panic. (See also `allow_parking`)
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
pub fn run_until_parked(&self) {
|
pub fn run_until_parked(&self) {
|
||||||
self.dispatcher.as_test().unwrap().run_until_parked()
|
self.dispatcher.as_test().unwrap().run_until_parked()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// in tests, prevents `run_until_parked` from panicking if there are outstanding tasks.
|
||||||
|
/// This is useful when you are integrating other (non-GPUI) futures, like disk access, that
|
||||||
|
/// do take real async time to run.
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
pub fn allow_parking(&self) {
|
pub fn allow_parking(&self) {
|
||||||
self.dispatcher.as_test().unwrap().allow_parking();
|
self.dispatcher.as_test().unwrap().allow_parking();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// in tests, returns the rng used by the dispatcher and seeded by the `SEED` environment variable
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
pub fn rng(&self) -> StdRng {
|
pub fn rng(&self) -> StdRng {
|
||||||
self.dispatcher.as_test().unwrap().rng()
|
self.dispatcher.as_test().unwrap().rng()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// How many CPUs are available to the dispatcher
|
||||||
pub fn num_cpus(&self) -> usize {
|
pub fn num_cpus(&self) -> usize {
|
||||||
num_cpus::get()
|
num_cpus::get()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether we're on the main thread.
|
||||||
pub fn is_main_thread(&self) -> bool {
|
pub fn is_main_thread(&self) -> bool {
|
||||||
self.dispatcher.is_main_thread()
|
self.dispatcher.is_main_thread()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
/// in tests, control the number of ticks that `block_with_timeout` will run before timing out.
|
||||||
pub fn set_block_on_ticks(&self, range: std::ops::RangeInclusive<usize>) {
|
pub fn set_block_on_ticks(&self, range: std::ops::RangeInclusive<usize>) {
|
||||||
self.dispatcher.as_test().unwrap().set_block_on_ticks(range);
|
self.dispatcher.as_test().unwrap().set_block_on_ticks(range);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// ForegroundExecutor runs things on the main thread.
|
||||||
impl ForegroundExecutor {
|
impl ForegroundExecutor {
|
||||||
pub fn new(dispatcher: Arc<dyn PlatformDispatcher>) -> Self {
|
pub fn new(dispatcher: Arc<dyn PlatformDispatcher>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
@ -329,8 +369,7 @@ impl ForegroundExecutor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Enqueues the given closure to be run on any thread. The closure returns
|
/// Enqueues the given Task to run on the main thread at some point in the future.
|
||||||
/// a future which will be run to completion on any available thread.
|
|
||||||
pub fn spawn<R>(&self, future: impl Future<Output = R> + 'static) -> Task<R>
|
pub fn spawn<R>(&self, future: impl Future<Output = R> + 'static) -> Task<R>
|
||||||
where
|
where
|
||||||
R: 'static,
|
R: 'static,
|
||||||
|
@ -350,6 +389,7 @@ impl ForegroundExecutor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Scope manages a set of tasks that are enqueued and waited on together. See [`BackgroundExecutor::scoped`].
|
||||||
pub struct Scope<'a> {
|
pub struct Scope<'a> {
|
||||||
executor: BackgroundExecutor,
|
executor: BackgroundExecutor,
|
||||||
futures: Vec<Pin<Box<dyn Future<Output = ()> + Send + 'static>>>,
|
futures: Vec<Pin<Box<dyn Future<Output = ()> + Send + 'static>>>,
|
||||||
|
|
|
@ -18,6 +18,7 @@ mod platform;
|
||||||
pub mod prelude;
|
pub mod prelude;
|
||||||
mod scene;
|
mod scene;
|
||||||
mod shared_string;
|
mod shared_string;
|
||||||
|
mod shared_url;
|
||||||
mod style;
|
mod style;
|
||||||
mod styled;
|
mod styled;
|
||||||
mod subscription;
|
mod subscription;
|
||||||
|
@ -67,6 +68,7 @@ pub use refineable::*;
|
||||||
pub use scene::*;
|
pub use scene::*;
|
||||||
use seal::Sealed;
|
use seal::Sealed;
|
||||||
pub use shared_string::*;
|
pub use shared_string::*;
|
||||||
|
pub use shared_url::*;
|
||||||
pub use smol::Timer;
|
pub use smol::Timer;
|
||||||
pub use style::*;
|
pub use style::*;
|
||||||
pub use styled::*;
|
pub use styled::*;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::{ImageData, ImageId, SharedString};
|
use crate::{ImageData, ImageId, SharedUrl};
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use futures::{
|
use futures::{
|
||||||
future::{BoxFuture, Shared},
|
future::{BoxFuture, Shared},
|
||||||
|
@ -44,7 +44,7 @@ impl From<ImageError> for Error {
|
||||||
|
|
||||||
pub struct ImageCache {
|
pub struct ImageCache {
|
||||||
client: Arc<dyn HttpClient>,
|
client: Arc<dyn HttpClient>,
|
||||||
images: Arc<Mutex<HashMap<SharedString, FetchImageFuture>>>,
|
images: Arc<Mutex<HashMap<SharedUrl, FetchImageFuture>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
type FetchImageFuture = Shared<BoxFuture<'static, Result<Arc<ImageData>, Error>>>;
|
type FetchImageFuture = Shared<BoxFuture<'static, Result<Arc<ImageData>, Error>>>;
|
||||||
|
@ -59,7 +59,7 @@ impl ImageCache {
|
||||||
|
|
||||||
pub fn get(
|
pub fn get(
|
||||||
&self,
|
&self,
|
||||||
uri: impl Into<SharedString>,
|
uri: impl Into<SharedUrl>,
|
||||||
) -> Shared<BoxFuture<'static, Result<Arc<ImageData>, Error>>> {
|
) -> Shared<BoxFuture<'static, Result<Arc<ImageData>, Error>>> {
|
||||||
let uri = uri.into();
|
let uri = uri.into();
|
||||||
let mut images = self.images.lock();
|
let mut images = self.images.lock();
|
||||||
|
|
|
@ -34,7 +34,7 @@ pub trait InputHandler: 'static + Sized {
|
||||||
) -> Option<Bounds<Pixels>>;
|
) -> Option<Bounds<Pixels>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The canonical implementation of `PlatformInputHandler`. Call `WindowContext::handle_input`
|
/// The canonical implementation of [`PlatformInputHandler`]. Call [`WindowContext::handle_input`]
|
||||||
/// with an instance during your element's paint.
|
/// with an instance during your element's paint.
|
||||||
pub struct ElementInputHandler<V> {
|
pub struct ElementInputHandler<V> {
|
||||||
view: View<V>,
|
view: View<V>,
|
||||||
|
|
|
@ -178,6 +178,20 @@ impl ScrollDelta {
|
||||||
ScrollDelta::Lines(delta) => point(line_height * delta.x, line_height * delta.y),
|
ScrollDelta::Lines(delta) => point(line_height * delta.x, line_height * delta.y),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn coalesce(self, other: ScrollDelta) -> ScrollDelta {
|
||||||
|
match (self, other) {
|
||||||
|
(ScrollDelta::Pixels(px_a), ScrollDelta::Pixels(px_b)) => {
|
||||||
|
ScrollDelta::Pixels(px_a + px_b)
|
||||||
|
}
|
||||||
|
|
||||||
|
(ScrollDelta::Lines(lines_a), ScrollDelta::Lines(lines_b)) => {
|
||||||
|
ScrollDelta::Lines(lines_a + lines_b)
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => other,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
|
|
|
@ -192,8 +192,8 @@ impl DispatchTree {
|
||||||
keymap
|
keymap
|
||||||
.bindings_for_action(action)
|
.bindings_for_action(action)
|
||||||
.filter(|binding| {
|
.filter(|binding| {
|
||||||
for i in 1..context_stack.len() {
|
for i in 0..context_stack.len() {
|
||||||
let context = &context_stack[0..i];
|
let context = &context_stack[0..=i];
|
||||||
if keymap.binding_enabled(binding, context) {
|
if keymap.binding_enabled(binding, context) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -283,3 +283,76 @@ impl DispatchTree {
|
||||||
*self.node_stack.last().unwrap()
|
*self.node_stack.last().unwrap()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::{rc::Rc, sync::Arc};
|
||||||
|
|
||||||
|
use parking_lot::Mutex;
|
||||||
|
|
||||||
|
use crate::{Action, ActionRegistry, DispatchTree, KeyBinding, KeyContext, Keymap};
|
||||||
|
|
||||||
|
#[derive(PartialEq, Eq)]
|
||||||
|
struct TestAction;
|
||||||
|
|
||||||
|
impl Action for TestAction {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"test::TestAction"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn debug_name() -> &'static str
|
||||||
|
where
|
||||||
|
Self: ::std::marker::Sized,
|
||||||
|
{
|
||||||
|
"test::TestAction"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn partial_eq(&self, action: &dyn Action) -> bool {
|
||||||
|
action
|
||||||
|
.as_any()
|
||||||
|
.downcast_ref::<Self>()
|
||||||
|
.map_or(false, |a| self == a)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn boxed_clone(&self) -> std::boxed::Box<dyn Action> {
|
||||||
|
Box::new(TestAction)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_any(&self) -> &dyn ::std::any::Any {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build(_value: serde_json::Value) -> anyhow::Result<Box<dyn Action>>
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
Ok(Box::new(TestAction))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_keybinding_for_action_bounds() {
|
||||||
|
let keymap = Keymap::new(vec![KeyBinding::new(
|
||||||
|
"cmd-n",
|
||||||
|
TestAction,
|
||||||
|
Some("ProjectPanel"),
|
||||||
|
)]);
|
||||||
|
|
||||||
|
let mut registry = ActionRegistry::default();
|
||||||
|
|
||||||
|
registry.load_action::<TestAction>();
|
||||||
|
|
||||||
|
let keymap = Arc::new(Mutex::new(keymap));
|
||||||
|
|
||||||
|
let tree = DispatchTree::new(keymap, Rc::new(registry));
|
||||||
|
|
||||||
|
let contexts = vec![
|
||||||
|
KeyContext::parse("Workspace").unwrap(),
|
||||||
|
KeyContext::parse("ProjectPanel").unwrap(),
|
||||||
|
];
|
||||||
|
|
||||||
|
let keybinding = tree.bindings_for_action(&TestAction, &contexts);
|
||||||
|
|
||||||
|
assert!(keybinding[0].action.partial_eq(&TestAction))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -14,12 +14,12 @@ pub struct MacDisplay(pub(crate) CGDirectDisplayID);
|
||||||
unsafe impl Send for MacDisplay {}
|
unsafe impl Send for MacDisplay {}
|
||||||
|
|
||||||
impl MacDisplay {
|
impl MacDisplay {
|
||||||
/// Get the screen with the given [DisplayId].
|
/// Get the screen with the given [`DisplayId`].
|
||||||
pub fn find_by_id(id: DisplayId) -> Option<Self> {
|
pub fn find_by_id(id: DisplayId) -> Option<Self> {
|
||||||
Self::all().find(|screen| screen.id() == id)
|
Self::all().find(|screen| screen.id() == id)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the screen with the given persistent [Uuid].
|
/// Get the screen with the given persistent [`Uuid`].
|
||||||
pub fn find_by_uuid(uuid: Uuid) -> Option<Self> {
|
pub fn find_by_uuid(uuid: Uuid) -> Option<Self> {
|
||||||
Self::all().find(|screen| screen.uuid().ok() == Some(uuid))
|
Self::all().find(|screen| screen.uuid().ok() == Some(uuid))
|
||||||
}
|
}
|
||||||
|
|
|
@ -338,6 +338,7 @@ struct MacWindowState {
|
||||||
ime_state: ImeState,
|
ime_state: ImeState,
|
||||||
// Retains the last IME Text
|
// Retains the last IME Text
|
||||||
ime_text: Option<String>,
|
ime_text: Option<String>,
|
||||||
|
external_files_dragged: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MacWindowState {
|
impl MacWindowState {
|
||||||
|
@ -567,6 +568,7 @@ impl MacWindow {
|
||||||
previous_modifiers_changed_event: None,
|
previous_modifiers_changed_event: None,
|
||||||
ime_state: ImeState::None,
|
ime_state: ImeState::None,
|
||||||
ime_text: None,
|
ime_text: None,
|
||||||
|
external_files_dragged: false,
|
||||||
})));
|
})));
|
||||||
|
|
||||||
(*native_window).set_ivar(
|
(*native_window).set_ivar(
|
||||||
|
@ -1223,15 +1225,20 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
|
||||||
..
|
..
|
||||||
},
|
},
|
||||||
) => {
|
) => {
|
||||||
lock.synthetic_drag_counter += 1;
|
// Synthetic drag is used for selecting long buffer contents while buffer is being scrolled.
|
||||||
let executor = lock.executor.clone();
|
// External file drag and drop is able to emit its own synthetic mouse events which will conflict
|
||||||
executor
|
// with these ones.
|
||||||
.spawn(synthetic_drag(
|
if !lock.external_files_dragged {
|
||||||
weak_window_state,
|
lock.synthetic_drag_counter += 1;
|
||||||
lock.synthetic_drag_counter,
|
let executor = lock.executor.clone();
|
||||||
event.clone(),
|
executor
|
||||||
))
|
.spawn(synthetic_drag(
|
||||||
.detach();
|
weak_window_state,
|
||||||
|
lock.synthetic_drag_counter,
|
||||||
|
event.clone(),
|
||||||
|
))
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
InputEvent::MouseMove(_) if !(is_active || lock.kind == WindowKind::PopUp) => return,
|
InputEvent::MouseMove(_) if !(is_active || lock.kind == WindowKind::PopUp) => return,
|
||||||
|
@ -1675,6 +1682,7 @@ extern "C" fn dragging_entered(this: &Object, _: Sel, dragging_info: id) -> NSDr
|
||||||
let paths = external_paths_from_event(dragging_info);
|
let paths = external_paths_from_event(dragging_info);
|
||||||
InputEvent::FileDrop(FileDropEvent::Entered { position, paths })
|
InputEvent::FileDrop(FileDropEvent::Entered { position, paths })
|
||||||
}) {
|
}) {
|
||||||
|
window_state.lock().external_files_dragged = true;
|
||||||
NSDragOperationCopy
|
NSDragOperationCopy
|
||||||
} else {
|
} else {
|
||||||
NSDragOperationNone
|
NSDragOperationNone
|
||||||
|
@ -1697,6 +1705,7 @@ extern "C" fn dragging_updated(this: &Object, _: Sel, dragging_info: id) -> NSDr
|
||||||
extern "C" fn dragging_exited(this: &Object, _: Sel, _: id) {
|
extern "C" fn dragging_exited(this: &Object, _: Sel, _: id) {
|
||||||
let window_state = unsafe { get_window_state(this) };
|
let window_state = unsafe { get_window_state(this) };
|
||||||
send_new_event(&window_state, InputEvent::FileDrop(FileDropEvent::Exited));
|
send_new_event(&window_state, InputEvent::FileDrop(FileDropEvent::Exited));
|
||||||
|
window_state.lock().external_files_dragged = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
extern "C" fn perform_drag_operation(this: &Object, _: Sel, dragging_info: id) -> BOOL {
|
extern "C" fn perform_drag_operation(this: &Object, _: Sel, dragging_info: id) -> BOOL {
|
||||||
|
|
|
@ -32,7 +32,7 @@ impl PlatformDisplay for TestDisplay {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn as_any(&self) -> &dyn std::any::Any {
|
fn as_any(&self) -> &dyn std::any::Any {
|
||||||
todo!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn bounds(&self) -> crate::Bounds<crate::GlobalPixels> {
|
fn bounds(&self) -> crate::Bounds<crate::GlobalPixels> {
|
||||||
|
|
|
@ -15,6 +15,7 @@ use std::{
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// TestPlatform implements the Platform trait for use in tests.
|
||||||
pub struct TestPlatform {
|
pub struct TestPlatform {
|
||||||
background_executor: BackgroundExecutor,
|
background_executor: BackgroundExecutor,
|
||||||
foreground_executor: ForegroundExecutor,
|
foreground_executor: ForegroundExecutor,
|
||||||
|
@ -101,9 +102,12 @@ impl TestPlatform {
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn did_prompt_for_new_path(&self) -> bool {
|
||||||
|
self.prompts.borrow().new_path.len() > 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// todo!("implement out what our tests needed in GPUI 1")
|
|
||||||
impl Platform for TestPlatform {
|
impl Platform for TestPlatform {
|
||||||
fn background_executor(&self) -> BackgroundExecutor {
|
fn background_executor(&self) -> BackgroundExecutor {
|
||||||
self.background_executor.clone()
|
self.background_executor.clone()
|
||||||
|
@ -278,8 +282,7 @@ impl Platform for TestPlatform {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn should_auto_hide_scrollbars(&self) -> bool {
|
fn should_auto_hide_scrollbars(&self) -> bool {
|
||||||
// todo()
|
false
|
||||||
true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_to_clipboard(&self, item: ClipboardItem) {
|
fn write_to_clipboard(&self, item: ClipboardItem) {
|
||||||
|
|
25
crates/gpui/src/shared_url.rs
Normal file
25
crates/gpui/src/shared_url.rs
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
use derive_more::{Deref, DerefMut};
|
||||||
|
|
||||||
|
use crate::SharedString;
|
||||||
|
|
||||||
|
/// A [`SharedString`] containing a URL.
|
||||||
|
#[derive(Deref, DerefMut, Default, PartialEq, Eq, Hash, Clone)]
|
||||||
|
pub struct SharedUrl(SharedString);
|
||||||
|
|
||||||
|
impl std::fmt::Debug for SharedUrl {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
self.0.fmt(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for SharedUrl {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}", self.0.as_ref())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Into<SharedString>> From<T> for SharedUrl {
|
||||||
|
fn from(value: T) -> Self {
|
||||||
|
Self(value.into())
|
||||||
|
}
|
||||||
|
}
|
|
@ -165,7 +165,8 @@ impl Default for TextStyle {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
TextStyle {
|
TextStyle {
|
||||||
color: black(),
|
color: black(),
|
||||||
font_family: "Helvetica".into(), // todo!("Get a font we know exists on the system")
|
// Helvetica is a web safe font, so it should be available
|
||||||
|
font_family: "Helvetica".into(),
|
||||||
font_features: FontFeatures::default(),
|
font_features: FontFeatures::default(),
|
||||||
font_size: rems(1.).into(),
|
font_size: rems(1.).into(),
|
||||||
line_height: phi(),
|
line_height: phi(),
|
||||||
|
|
|
@ -37,10 +37,10 @@ where
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Inserts a new `[Subscription]` for the given `emitter_key`. By default, subscriptions
|
/// Inserts a new [`Subscription`] for the given `emitter_key`. By default, subscriptions
|
||||||
/// are inert, meaning that they won't be listed when calling `[SubscriberSet::remove]` or `[SubscriberSet::retain]`.
|
/// are inert, meaning that they won't be listed when calling `[SubscriberSet::remove]` or `[SubscriberSet::retain]`.
|
||||||
/// This method returns a tuple of a `[Subscription]` and an `impl FnOnce`, and you can use the latter
|
/// This method returns a tuple of a [`Subscription`] and an `impl FnOnce`, and you can use the latter
|
||||||
/// to activate the `[Subscription]`.
|
/// to activate the [`Subscription`].
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn insert(
|
pub fn insert(
|
||||||
&self,
|
&self,
|
||||||
|
|
|
@ -1,3 +1,30 @@
|
||||||
|
//! Test support for GPUI.
|
||||||
|
//!
|
||||||
|
//! GPUI provides first-class support for testing, which includes a macro to run test that rely on having a context,
|
||||||
|
//! and a test implementation of the `ForegroundExecutor` and `BackgroundExecutor` which ensure that your tests run
|
||||||
|
//! deterministically even in the face of arbitrary parallelism.
|
||||||
|
//!
|
||||||
|
//! The output of the `gpui::test` macro is understood by other rust test runners, so you can use it with `cargo test`
|
||||||
|
//! or `cargo-nextest`, or another runner of your choice.
|
||||||
|
//!
|
||||||
|
//! To make it possible to test collaborative user interfaces (like Zed) you can ask for as many different contexts
|
||||||
|
//! as you need.
|
||||||
|
//!
|
||||||
|
//! ## Example
|
||||||
|
//!
|
||||||
|
//! ```
|
||||||
|
//! use gpui;
|
||||||
|
//!
|
||||||
|
//! #[gpui::test]
|
||||||
|
//! async fn test_example(cx: &TestAppContext) {
|
||||||
|
//! assert!(true)
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! #[gpui::test]
|
||||||
|
//! async fn test_collaboration_example(cx_a: &TestAppContext, cx_b: &TestAppContext) {
|
||||||
|
//! assert!(true)
|
||||||
|
//! }
|
||||||
|
//! ```
|
||||||
use crate::{Entity, Subscription, TestAppContext, TestDispatcher};
|
use crate::{Entity, Subscription, TestAppContext, TestDispatcher};
|
||||||
use futures::StreamExt as _;
|
use futures::StreamExt as _;
|
||||||
use rand::prelude::*;
|
use rand::prelude::*;
|
||||||
|
@ -12,7 +39,6 @@ pub fn run_test(
|
||||||
max_retries: usize,
|
max_retries: usize,
|
||||||
test_fn: &mut (dyn RefUnwindSafe + Fn(TestDispatcher, u64)),
|
test_fn: &mut (dyn RefUnwindSafe + Fn(TestDispatcher, u64)),
|
||||||
on_fail_fn: Option<fn()>,
|
on_fail_fn: Option<fn()>,
|
||||||
_fn_name: String, // todo!("re-enable fn_name")
|
|
||||||
) {
|
) {
|
||||||
let starting_seed = env::var("SEED")
|
let starting_seed = env::var("SEED")
|
||||||
.map(|seed| seed.parse().expect("invalid SEED variable"))
|
.map(|seed| seed.parse().expect("invalid SEED variable"))
|
||||||
|
@ -68,6 +94,7 @@ impl<T: 'static> futures::Stream for Observation<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// observe returns a stream of the change events from the given `View` or `Model`
|
||||||
pub fn observe<T: 'static>(entity: &impl Entity<T>, cx: &mut TestAppContext) -> Observation<()> {
|
pub fn observe<T: 'static>(entity: &impl Entity<T>, cx: &mut TestAppContext) -> Observation<()> {
|
||||||
let (tx, rx) = smol::channel::unbounded();
|
let (tx, rx) = smol::channel::unbounded();
|
||||||
let _subscription = cx.update(|cx| {
|
let _subscription = cx.update(|cx| {
|
||||||
|
|
|
@ -258,7 +258,7 @@ impl TextSystem {
|
||||||
|
|
||||||
pub fn shape_text(
|
pub fn shape_text(
|
||||||
&self,
|
&self,
|
||||||
text: &str, // todo!("pass a SharedString and preserve it when passed a single line?")
|
text: SharedString,
|
||||||
font_size: Pixels,
|
font_size: Pixels,
|
||||||
runs: &[TextRun],
|
runs: &[TextRun],
|
||||||
wrap_width: Option<Pixels>,
|
wrap_width: Option<Pixels>,
|
||||||
|
@ -268,8 +268,8 @@ impl TextSystem {
|
||||||
|
|
||||||
let mut lines = SmallVec::new();
|
let mut lines = SmallVec::new();
|
||||||
let mut line_start = 0;
|
let mut line_start = 0;
|
||||||
for line_text in text.split('\n') {
|
|
||||||
let line_text = SharedString::from(line_text.to_string());
|
let mut process_line = |line_text: SharedString| {
|
||||||
let line_end = line_start + line_text.len();
|
let line_end = line_start + line_text.len();
|
||||||
|
|
||||||
let mut last_font: Option<Font> = None;
|
let mut last_font: Option<Font> = None;
|
||||||
|
@ -335,6 +335,24 @@ impl TextSystem {
|
||||||
}
|
}
|
||||||
|
|
||||||
font_runs.clear();
|
font_runs.clear();
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut split_lines = text.split('\n');
|
||||||
|
let mut processed = false;
|
||||||
|
|
||||||
|
if let Some(first_line) = split_lines.next() {
|
||||||
|
if let Some(second_line) = split_lines.next() {
|
||||||
|
processed = true;
|
||||||
|
process_line(first_line.to_string().into());
|
||||||
|
process_line(second_line.to_string().into());
|
||||||
|
for line_text in split_lines {
|
||||||
|
process_line(line_text.to_string().into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !processed {
|
||||||
|
process_line(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.font_runs_pool.lock().push(font_runs);
|
self.font_runs_pool.lock().push(font_runs);
|
||||||
|
|
|
@ -143,7 +143,7 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_wrap_line() {
|
fn test_wrap_line() {
|
||||||
let dispatcher = TestDispatcher::new(StdRng::seed_from_u64(0));
|
let dispatcher = TestDispatcher::new(StdRng::seed_from_u64(0));
|
||||||
let cx = TestAppContext::new(dispatcher);
|
let cx = TestAppContext::new(dispatcher, None);
|
||||||
|
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
let text_system = cx.text_system().clone();
|
let text_system = cx.text_system().clone();
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#![deny(missing_docs)]
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
px, size, transparent_black, Action, AnyDrag, AnyView, AppContext, Arena, ArenaBox, ArenaRef,
|
px, size, transparent_black, Action, AnyDrag, AnyView, AppContext, Arena, ArenaBox, ArenaRef,
|
||||||
AsyncWindowContext, AvailableSpace, Bounds, BoxShadow, Context, Corners, CursorStyle,
|
AsyncWindowContext, AvailableSpace, Bounds, BoxShadow, Context, Corners, CursorStyle,
|
||||||
|
@ -85,10 +87,12 @@ pub enum DispatchPhase {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DispatchPhase {
|
impl DispatchPhase {
|
||||||
|
/// Returns true if this represents the "bubble" phase.
|
||||||
pub fn bubble(self) -> bool {
|
pub fn bubble(self) -> bool {
|
||||||
self == DispatchPhase::Bubble
|
self == DispatchPhase::Bubble
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns true if this represents the "capture" phase.
|
||||||
pub fn capture(self) -> bool {
|
pub fn capture(self) -> bool {
|
||||||
self == DispatchPhase::Capture
|
self == DispatchPhase::Capture
|
||||||
}
|
}
|
||||||
|
@ -103,7 +107,10 @@ struct FocusEvent {
|
||||||
current_focus_path: SmallVec<[FocusId; 8]>,
|
current_focus_path: SmallVec<[FocusId; 8]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
slotmap::new_key_type! { pub struct FocusId; }
|
slotmap::new_key_type! {
|
||||||
|
/// A globally unique identifier for a focusable element.
|
||||||
|
pub struct FocusId;
|
||||||
|
}
|
||||||
|
|
||||||
thread_local! {
|
thread_local! {
|
||||||
pub(crate) static ELEMENT_ARENA: RefCell<Arena> = RefCell::new(Arena::new(4 * 1024 * 1024));
|
pub(crate) static ELEMENT_ARENA: RefCell<Arena> = RefCell::new(Arena::new(4 * 1024 * 1024));
|
||||||
|
@ -231,6 +238,7 @@ impl Drop for FocusHandle {
|
||||||
/// FocusableView allows users of your view to easily
|
/// FocusableView allows users of your view to easily
|
||||||
/// focus it (using cx.focus_view(view))
|
/// focus it (using cx.focus_view(view))
|
||||||
pub trait FocusableView: 'static + Render {
|
pub trait FocusableView: 'static + Render {
|
||||||
|
/// Returns the focus handle associated with this view.
|
||||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle;
|
fn focus_handle(&self, cx: &AppContext) -> FocusHandle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -240,9 +248,11 @@ pub trait ManagedView: FocusableView + EventEmitter<DismissEvent> {}
|
||||||
|
|
||||||
impl<M: FocusableView + EventEmitter<DismissEvent>> ManagedView for M {}
|
impl<M: FocusableView + EventEmitter<DismissEvent>> ManagedView for M {}
|
||||||
|
|
||||||
|
/// Emitted by implementers of [`ManagedView`] to indicate the view should be dismissed, such as when a view is presented as a modal.
|
||||||
pub struct DismissEvent;
|
pub struct DismissEvent;
|
||||||
|
|
||||||
// Holds the state for a specific window.
|
// Holds the state for a specific window.
|
||||||
|
#[doc(hidden)]
|
||||||
pub struct Window {
|
pub struct Window {
|
||||||
pub(crate) handle: AnyWindowHandle,
|
pub(crate) handle: AnyWindowHandle,
|
||||||
pub(crate) removed: bool,
|
pub(crate) removed: bool,
|
||||||
|
@ -259,7 +269,7 @@ pub struct Window {
|
||||||
frame_arena: Arena,
|
frame_arena: Arena,
|
||||||
pub(crate) focus_handles: Arc<RwLock<SlotMap<FocusId, AtomicUsize>>>,
|
pub(crate) focus_handles: Arc<RwLock<SlotMap<FocusId, AtomicUsize>>>,
|
||||||
focus_listeners: SubscriberSet<(), AnyWindowFocusListener>,
|
focus_listeners: SubscriberSet<(), AnyWindowFocusListener>,
|
||||||
blur_listeners: SubscriberSet<(), AnyObserver>,
|
focus_lost_listeners: SubscriberSet<(), AnyObserver>,
|
||||||
default_prevented: bool,
|
default_prevented: bool,
|
||||||
mouse_position: Point<Pixels>,
|
mouse_position: Point<Pixels>,
|
||||||
modifiers: Modifiers,
|
modifiers: Modifiers,
|
||||||
|
@ -286,6 +296,7 @@ pub(crate) struct ElementStateBox {
|
||||||
|
|
||||||
pub(crate) struct Frame {
|
pub(crate) struct Frame {
|
||||||
focus: Option<FocusId>,
|
focus: Option<FocusId>,
|
||||||
|
window_active: bool,
|
||||||
pub(crate) element_states: FxHashMap<GlobalElementId, ElementStateBox>,
|
pub(crate) element_states: FxHashMap<GlobalElementId, ElementStateBox>,
|
||||||
mouse_listeners: FxHashMap<TypeId, Vec<(StackingOrder, AnyMouseListener)>>,
|
mouse_listeners: FxHashMap<TypeId, Vec<(StackingOrder, AnyMouseListener)>>,
|
||||||
pub(crate) dispatch_tree: DispatchTree,
|
pub(crate) dispatch_tree: DispatchTree,
|
||||||
|
@ -301,6 +312,7 @@ impl Frame {
|
||||||
fn new(dispatch_tree: DispatchTree) -> Self {
|
fn new(dispatch_tree: DispatchTree) -> Self {
|
||||||
Frame {
|
Frame {
|
||||||
focus: None,
|
focus: None,
|
||||||
|
window_active: false,
|
||||||
element_states: FxHashMap::default(),
|
element_states: FxHashMap::default(),
|
||||||
mouse_listeners: FxHashMap::default(),
|
mouse_listeners: FxHashMap::default(),
|
||||||
dispatch_tree,
|
dispatch_tree,
|
||||||
|
@ -407,7 +419,7 @@ impl Window {
|
||||||
frame_arena: Arena::new(1024 * 1024),
|
frame_arena: Arena::new(1024 * 1024),
|
||||||
focus_handles: Arc::new(RwLock::new(SlotMap::with_key())),
|
focus_handles: Arc::new(RwLock::new(SlotMap::with_key())),
|
||||||
focus_listeners: SubscriberSet::new(),
|
focus_listeners: SubscriberSet::new(),
|
||||||
blur_listeners: SubscriberSet::new(),
|
focus_lost_listeners: SubscriberSet::new(),
|
||||||
default_prevented: true,
|
default_prevented: true,
|
||||||
mouse_position,
|
mouse_position,
|
||||||
modifiers,
|
modifiers,
|
||||||
|
@ -434,6 +446,7 @@ impl Window {
|
||||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
pub struct ContentMask<P: Clone + Default + Debug> {
|
pub struct ContentMask<P: Clone + Default + Debug> {
|
||||||
|
/// The bounds
|
||||||
pub bounds: Bounds<P>,
|
pub bounds: Bounds<P>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -453,8 +466,8 @@ impl ContentMask<Pixels> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Provides access to application state in the context of a single window. Derefs
|
/// Provides access to application state in the context of a single window. Derefs
|
||||||
/// to an `AppContext`, so you can also pass a `WindowContext` to any method that takes
|
/// to an [`AppContext`], so you can also pass a [`WindowContext`] to any method that takes
|
||||||
/// an `AppContext` and call any `AppContext` methods.
|
/// an [`AppContext`] and call any [`AppContext`] methods.
|
||||||
pub struct WindowContext<'a> {
|
pub struct WindowContext<'a> {
|
||||||
pub(crate) app: &'a mut AppContext,
|
pub(crate) app: &'a mut AppContext,
|
||||||
pub(crate) window: &'a mut Window,
|
pub(crate) window: &'a mut Window,
|
||||||
|
@ -482,20 +495,20 @@ impl<'a> WindowContext<'a> {
|
||||||
self.window.removed = true;
|
self.window.removed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Obtain a new `FocusHandle`, which allows you to track and manipulate the keyboard focus
|
/// Obtain a new [`FocusHandle`], which allows you to track and manipulate the keyboard focus
|
||||||
/// for elements rendered within this window.
|
/// for elements rendered within this window.
|
||||||
pub fn focus_handle(&mut self) -> FocusHandle {
|
pub fn focus_handle(&mut self) -> FocusHandle {
|
||||||
FocusHandle::new(&self.window.focus_handles)
|
FocusHandle::new(&self.window.focus_handles)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Obtain the currently focused `FocusHandle`. If no elements are focused, returns `None`.
|
/// Obtain the currently focused [`FocusHandle`]. If no elements are focused, returns `None`.
|
||||||
pub fn focused(&self) -> Option<FocusHandle> {
|
pub fn focused(&self) -> Option<FocusHandle> {
|
||||||
self.window
|
self.window
|
||||||
.focus
|
.focus
|
||||||
.and_then(|id| FocusHandle::for_id(id, &self.window.focus_handles))
|
.and_then(|id| FocusHandle::for_id(id, &self.window.focus_handles))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Move focus to the element associated with the given `FocusHandle`.
|
/// Move focus to the element associated with the given [`FocusHandle`].
|
||||||
pub fn focus(&mut self, handle: &FocusHandle) {
|
pub fn focus(&mut self, handle: &FocusHandle) {
|
||||||
if !self.window.focus_enabled || self.window.focus == Some(handle.id) {
|
if !self.window.focus_enabled || self.window.focus == Some(handle.id) {
|
||||||
return;
|
return;
|
||||||
|
@ -525,11 +538,13 @@ impl<'a> WindowContext<'a> {
|
||||||
self.notify();
|
self.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Blur the window and don't allow anything in it to be focused again.
|
||||||
pub fn disable_focus(&mut self) {
|
pub fn disable_focus(&mut self) {
|
||||||
self.blur();
|
self.blur();
|
||||||
self.window.focus_enabled = false;
|
self.window.focus_enabled = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Dispatch the given action on the currently focused element.
|
||||||
pub fn dispatch_action(&mut self, action: Box<dyn Action>) {
|
pub fn dispatch_action(&mut self, action: Box<dyn Action>) {
|
||||||
let focus_handle = self.focused();
|
let focus_handle = self.focused();
|
||||||
|
|
||||||
|
@ -591,6 +606,9 @@ impl<'a> WindowContext<'a> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Subscribe to events emitted by a model or view.
|
||||||
|
/// The entity to which you're subscribing must implement the [`EventEmitter`] trait.
|
||||||
|
/// The callback will be invoked a handle to the emitting entity (either a [`View`] or [`Model`]), the event, and a window context for the current window.
|
||||||
pub fn subscribe<Emitter, E, Evt>(
|
pub fn subscribe<Emitter, E, Evt>(
|
||||||
&mut self,
|
&mut self,
|
||||||
entity: &E,
|
entity: &E,
|
||||||
|
@ -754,6 +772,9 @@ impl<'a> WindowContext<'a> {
|
||||||
.request_measured_layout(style, rem_size, measure)
|
.request_measured_layout(style, rem_size, measure)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Compute the layout for the given id within the given available space.
|
||||||
|
/// This method is called for its side effect, typically by the framework prior to painting.
|
||||||
|
/// After calling it, you can request the bounds of the given layout node id or any descendant.
|
||||||
pub fn compute_layout(&mut self, layout_id: LayoutId, available_space: Size<AvailableSpace>) {
|
pub fn compute_layout(&mut self, layout_id: LayoutId, available_space: Size<AvailableSpace>) {
|
||||||
let mut layout_engine = self.window.layout_engine.take().unwrap();
|
let mut layout_engine = self.window.layout_engine.take().unwrap();
|
||||||
layout_engine.compute_layout(layout_id, available_space, self);
|
layout_engine.compute_layout(layout_id, available_space, self);
|
||||||
|
@ -788,30 +809,37 @@ impl<'a> WindowContext<'a> {
|
||||||
.retain(&(), |callback| callback(self));
|
.retain(&(), |callback| callback(self));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the bounds of the current window in the global coordinate space, which could span across multiple displays.
|
||||||
pub fn window_bounds(&self) -> WindowBounds {
|
pub fn window_bounds(&self) -> WindowBounds {
|
||||||
self.window.bounds
|
self.window.bounds
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the size of the drawable area within the window.
|
||||||
pub fn viewport_size(&self) -> Size<Pixels> {
|
pub fn viewport_size(&self) -> Size<Pixels> {
|
||||||
self.window.viewport_size
|
self.window.viewport_size
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns whether this window is focused by the operating system (receiving key events).
|
||||||
pub fn is_window_active(&self) -> bool {
|
pub fn is_window_active(&self) -> bool {
|
||||||
self.window.active
|
self.window.active
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Toggle zoom on the window.
|
||||||
pub fn zoom_window(&self) {
|
pub fn zoom_window(&self) {
|
||||||
self.window.platform_window.zoom();
|
self.window.platform_window.zoom();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Update the window's title at the platform level.
|
||||||
pub fn set_window_title(&mut self, title: &str) {
|
pub fn set_window_title(&mut self, title: &str) {
|
||||||
self.window.platform_window.set_title(title);
|
self.window.platform_window.set_title(title);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Mark the window as dirty at the platform level.
|
||||||
pub fn set_window_edited(&mut self, edited: bool) {
|
pub fn set_window_edited(&mut self, edited: bool) {
|
||||||
self.window.platform_window.set_edited(edited);
|
self.window.platform_window.set_edited(edited);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Determine the display on which the window is visible.
|
||||||
pub fn display(&self) -> Option<Rc<dyn PlatformDisplay>> {
|
pub fn display(&self) -> Option<Rc<dyn PlatformDisplay>> {
|
||||||
self.platform
|
self.platform
|
||||||
.displays()
|
.displays()
|
||||||
|
@ -819,6 +847,7 @@ impl<'a> WindowContext<'a> {
|
||||||
.find(|display| display.id() == self.window.display_id)
|
.find(|display| display.id() == self.window.display_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Show the platform character palette.
|
||||||
pub fn show_character_palette(&self) {
|
pub fn show_character_palette(&self) {
|
||||||
self.window.platform_window.show_character_palette();
|
self.window.platform_window.show_character_palette();
|
||||||
}
|
}
|
||||||
|
@ -936,6 +965,7 @@ impl<'a> WindowContext<'a> {
|
||||||
.on_action(action_type, ArenaRef::from(listener));
|
.on_action(action_type, ArenaRef::from(listener));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Determine whether the given action is available along the dispatch path to the currently focused element.
|
||||||
pub fn is_action_available(&self, action: &dyn Action) -> bool {
|
pub fn is_action_available(&self, action: &dyn Action) -> bool {
|
||||||
let target = self
|
let target = self
|
||||||
.focused()
|
.focused()
|
||||||
|
@ -962,6 +992,7 @@ impl<'a> WindowContext<'a> {
|
||||||
self.window.modifiers
|
self.window.modifiers
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Update the cursor style at the platform level.
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
@ -991,7 +1022,7 @@ impl<'a> WindowContext<'a> {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn was_top_layer_under_active_drag(
|
pub(crate) fn was_top_layer_under_active_drag(
|
||||||
&self,
|
&self,
|
||||||
point: &Point<Pixels>,
|
point: &Point<Pixels>,
|
||||||
level: &StackingOrder,
|
level: &StackingOrder,
|
||||||
|
@ -1377,29 +1408,14 @@ impl<'a> WindowContext<'a> {
|
||||||
self.window.focus,
|
self.window.focus,
|
||||||
);
|
);
|
||||||
self.window.next_frame.focus = self.window.focus;
|
self.window.next_frame.focus = self.window.focus;
|
||||||
|
self.window.next_frame.window_active = self.window.active;
|
||||||
self.window.root_view = Some(root_view);
|
self.window.root_view = Some(root_view);
|
||||||
|
|
||||||
let previous_focus_path = self.window.rendered_frame.focus_path();
|
let previous_focus_path = self.window.rendered_frame.focus_path();
|
||||||
|
let previous_window_active = self.window.rendered_frame.window_active;
|
||||||
mem::swap(&mut self.window.rendered_frame, &mut self.window.next_frame);
|
mem::swap(&mut self.window.rendered_frame, &mut self.window.next_frame);
|
||||||
let current_focus_path = self.window.rendered_frame.focus_path();
|
let current_focus_path = self.window.rendered_frame.focus_path();
|
||||||
|
let current_window_active = self.window.rendered_frame.window_active;
|
||||||
if previous_focus_path != current_focus_path {
|
|
||||||
if !previous_focus_path.is_empty() && current_focus_path.is_empty() {
|
|
||||||
self.window
|
|
||||||
.blur_listeners
|
|
||||||
.clone()
|
|
||||||
.retain(&(), |listener| listener(self));
|
|
||||||
}
|
|
||||||
|
|
||||||
let event = FocusEvent {
|
|
||||||
previous_focus_path,
|
|
||||||
current_focus_path,
|
|
||||||
};
|
|
||||||
self.window
|
|
||||||
.focus_listeners
|
|
||||||
.clone()
|
|
||||||
.retain(&(), |listener| listener(&event, self));
|
|
||||||
}
|
|
||||||
|
|
||||||
let scene = self.window.rendered_frame.scene_builder.build();
|
let scene = self.window.rendered_frame.scene_builder.build();
|
||||||
|
|
||||||
|
@ -1416,6 +1432,34 @@ impl<'a> WindowContext<'a> {
|
||||||
self.window.drawing = false;
|
self.window.drawing = false;
|
||||||
ELEMENT_ARENA.with_borrow_mut(|element_arena| element_arena.clear());
|
ELEMENT_ARENA.with_borrow_mut(|element_arena| element_arena.clear());
|
||||||
|
|
||||||
|
if previous_focus_path != current_focus_path
|
||||||
|
|| previous_window_active != current_window_active
|
||||||
|
{
|
||||||
|
if !previous_focus_path.is_empty() && current_focus_path.is_empty() {
|
||||||
|
self.window
|
||||||
|
.focus_lost_listeners
|
||||||
|
.clone()
|
||||||
|
.retain(&(), |listener| listener(self));
|
||||||
|
}
|
||||||
|
|
||||||
|
let event = FocusEvent {
|
||||||
|
previous_focus_path: if previous_window_active {
|
||||||
|
previous_focus_path
|
||||||
|
} else {
|
||||||
|
Default::default()
|
||||||
|
},
|
||||||
|
current_focus_path: if current_window_active {
|
||||||
|
current_focus_path
|
||||||
|
} else {
|
||||||
|
Default::default()
|
||||||
|
},
|
||||||
|
};
|
||||||
|
self.window
|
||||||
|
.focus_listeners
|
||||||
|
.clone()
|
||||||
|
.retain(&(), |listener| listener(&event, self));
|
||||||
|
}
|
||||||
|
|
||||||
scene
|
scene
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1445,9 +1489,7 @@ impl<'a> WindowContext<'a> {
|
||||||
InputEvent::MouseUp(mouse_up)
|
InputEvent::MouseUp(mouse_up)
|
||||||
}
|
}
|
||||||
InputEvent::MouseExited(mouse_exited) => {
|
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;
|
self.window.modifiers = mouse_exited.modifiers;
|
||||||
|
|
||||||
InputEvent::MouseExited(mouse_exited)
|
InputEvent::MouseExited(mouse_exited)
|
||||||
}
|
}
|
||||||
InputEvent::ModifiersChanged(modifiers_changed) => {
|
InputEvent::ModifiersChanged(modifiers_changed) => {
|
||||||
|
@ -1649,6 +1691,7 @@ impl<'a> WindowContext<'a> {
|
||||||
self.dispatch_keystroke_observers(event, None);
|
self.dispatch_keystroke_observers(event, None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Determine whether a potential multi-stroke key binding is in progress on this window.
|
||||||
pub fn has_pending_keystrokes(&self) -> bool {
|
pub fn has_pending_keystrokes(&self) -> bool {
|
||||||
self.window
|
self.window
|
||||||
.rendered_frame
|
.rendered_frame
|
||||||
|
@ -1715,27 +1758,34 @@ impl<'a> WindowContext<'a> {
|
||||||
subscription
|
subscription
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Focus the current window and bring it to the foreground at the platform level.
|
||||||
pub fn activate_window(&self) {
|
pub fn activate_window(&self) {
|
||||||
self.window.platform_window.activate();
|
self.window.platform_window.activate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Minimize the current window at the platform level.
|
||||||
pub fn minimize_window(&self) {
|
pub fn minimize_window(&self) {
|
||||||
self.window.platform_window.minimize();
|
self.window.platform_window.minimize();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Toggle full screen status on the current window at the platform level.
|
||||||
pub fn toggle_full_screen(&self) {
|
pub fn toggle_full_screen(&self) {
|
||||||
self.window.platform_window.toggle_full_screen();
|
self.window.platform_window.toggle_full_screen();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Present a platform dialog.
|
||||||
|
/// The provided message will be presented, along with buttons for each answer.
|
||||||
|
/// When a button is clicked, the returned Receiver will receive the index of the clicked button.
|
||||||
pub fn prompt(
|
pub fn prompt(
|
||||||
&self,
|
&self,
|
||||||
level: PromptLevel,
|
level: PromptLevel,
|
||||||
msg: &str,
|
message: &str,
|
||||||
answers: &[&str],
|
answers: &[&str],
|
||||||
) -> oneshot::Receiver<usize> {
|
) -> oneshot::Receiver<usize> {
|
||||||
self.window.platform_window.prompt(level, msg, answers)
|
self.window.platform_window.prompt(level, message, answers)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns all available actions for the focused element.
|
||||||
pub fn available_actions(&self) -> Vec<Box<dyn Action>> {
|
pub fn available_actions(&self) -> Vec<Box<dyn Action>> {
|
||||||
let node_id = self
|
let node_id = self
|
||||||
.window
|
.window
|
||||||
|
@ -1754,6 +1804,7 @@ impl<'a> WindowContext<'a> {
|
||||||
.available_actions(node_id)
|
.available_actions(node_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns key bindings that invoke the given action on the currently focused element.
|
||||||
pub fn bindings_for_action(&self, action: &dyn Action) -> Vec<KeyBinding> {
|
pub fn bindings_for_action(&self, action: &dyn Action) -> Vec<KeyBinding> {
|
||||||
self.window
|
self.window
|
||||||
.rendered_frame
|
.rendered_frame
|
||||||
|
@ -1764,6 +1815,7 @@ impl<'a> WindowContext<'a> {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns any bindings that would invoke the given action on the given focus handle if it were focused.
|
||||||
pub fn bindings_for_action_in(
|
pub fn bindings_for_action_in(
|
||||||
&self,
|
&self,
|
||||||
action: &dyn Action,
|
action: &dyn Action,
|
||||||
|
@ -1782,6 +1834,7 @@ impl<'a> WindowContext<'a> {
|
||||||
dispatch_tree.bindings_for_action(action, &context_stack)
|
dispatch_tree.bindings_for_action(action, &context_stack)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns a generic event listener that invokes the given listener with the view and context associated with the given view handle.
|
||||||
pub fn listener_for<V: Render, E>(
|
pub fn listener_for<V: Render, E>(
|
||||||
&self,
|
&self,
|
||||||
view: &View<V>,
|
view: &View<V>,
|
||||||
|
@ -1793,6 +1846,7 @@ impl<'a> WindowContext<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns a generic handler that invokes the given handler with the view and context associated with the given view handle.
|
||||||
pub fn handler_for<V: Render>(
|
pub fn handler_for<V: Render>(
|
||||||
&self,
|
&self,
|
||||||
view: &View<V>,
|
view: &View<V>,
|
||||||
|
@ -1804,7 +1858,8 @@ impl<'a> WindowContext<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//========== ELEMENT RELATED FUNCTIONS ===========
|
/// Invoke the given function with the given focus handle present on the key dispatch stack.
|
||||||
|
/// If you want an element to participate in key dispatch, use this method to push its key context and focus handle into the stack during paint.
|
||||||
pub fn with_key_dispatch<R>(
|
pub fn with_key_dispatch<R>(
|
||||||
&mut self,
|
&mut self,
|
||||||
context: Option<KeyContext>,
|
context: Option<KeyContext>,
|
||||||
|
@ -1843,6 +1898,8 @@ impl<'a> WindowContext<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Register a callback that can interrupt the closing of the current window based the returned boolean.
|
||||||
|
/// If the callback returns false, the window won't be closed.
|
||||||
pub fn on_window_should_close(&mut self, f: impl Fn(&mut WindowContext) -> bool + 'static) {
|
pub fn on_window_should_close(&mut self, f: impl Fn(&mut WindowContext) -> bool + 'static) {
|
||||||
let mut this = self.to_async();
|
let mut this = self.to_async();
|
||||||
self.window
|
self.window
|
||||||
|
@ -2017,19 +2074,24 @@ impl<'a> BorrowMut<AppContext> for WindowContext<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// This trait contains functionality that is shared across [`ViewContext`] and [`WindowContext`]
|
||||||
pub trait BorrowWindow: BorrowMut<Window> + BorrowMut<AppContext> {
|
pub trait BorrowWindow: BorrowMut<Window> + BorrowMut<AppContext> {
|
||||||
|
#[doc(hidden)]
|
||||||
fn app_mut(&mut self) -> &mut AppContext {
|
fn app_mut(&mut self) -> &mut AppContext {
|
||||||
self.borrow_mut()
|
self.borrow_mut()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
fn app(&self) -> &AppContext {
|
fn app(&self) -> &AppContext {
|
||||||
self.borrow()
|
self.borrow()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
fn window(&self) -> &Window {
|
fn window(&self) -> &Window {
|
||||||
self.borrow()
|
self.borrow()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
fn window_mut(&mut self) -> &mut Window {
|
fn window_mut(&mut self) -> &mut Window {
|
||||||
self.borrow_mut()
|
self.borrow_mut()
|
||||||
}
|
}
|
||||||
|
@ -2279,6 +2341,10 @@ impl BorrowMut<Window> for WindowContext<'_> {
|
||||||
|
|
||||||
impl<T> BorrowWindow for T where T: BorrowMut<AppContext> + BorrowMut<Window> {}
|
impl<T> BorrowWindow for T where T: BorrowMut<AppContext> + BorrowMut<Window> {}
|
||||||
|
|
||||||
|
/// Provides access to application state that is specialized for a particular [`View`].
|
||||||
|
/// Allows you to interact with focus, emit events, etc.
|
||||||
|
/// ViewContext also derefs to [`WindowContext`], giving you access to all of its methods as well.
|
||||||
|
/// When you call [`View::update`], you're passed a `&mut V` and an `&mut ViewContext<V>`.
|
||||||
pub struct ViewContext<'a, V> {
|
pub struct ViewContext<'a, V> {
|
||||||
window_cx: WindowContext<'a>,
|
window_cx: WindowContext<'a>,
|
||||||
view: &'a View<V>,
|
view: &'a View<V>,
|
||||||
|
@ -2316,14 +2382,17 @@ impl<'a, V: 'static> ViewContext<'a, V> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the entity_id of this view.
|
||||||
pub fn entity_id(&self) -> EntityId {
|
pub fn entity_id(&self) -> EntityId {
|
||||||
self.view.entity_id()
|
self.view.entity_id()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the view pointer underlying this context.
|
||||||
pub fn view(&self) -> &View<V> {
|
pub fn view(&self) -> &View<V> {
|
||||||
self.view
|
self.view
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the model underlying this view.
|
||||||
pub fn model(&self) -> &Model<V> {
|
pub fn model(&self) -> &Model<V> {
|
||||||
&self.view.model
|
&self.view.model
|
||||||
}
|
}
|
||||||
|
@ -2333,6 +2402,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
|
||||||
&mut self.window_cx
|
&mut self.window_cx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set a given callback to be run on the next frame.
|
||||||
pub fn on_next_frame(&mut self, f: impl FnOnce(&mut V, &mut ViewContext<V>) + 'static)
|
pub fn on_next_frame(&mut self, f: impl FnOnce(&mut V, &mut ViewContext<V>) + 'static)
|
||||||
where
|
where
|
||||||
V: 'static,
|
V: 'static,
|
||||||
|
@ -2350,6 +2420,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Observe another model or view for changes to its state, as tracked by [`ModelContext::notify`].
|
||||||
pub fn observe<V2, E>(
|
pub fn observe<V2, E>(
|
||||||
&mut self,
|
&mut self,
|
||||||
entity: &E,
|
entity: &E,
|
||||||
|
@ -2383,6 +2454,9 @@ impl<'a, V: 'static> ViewContext<'a, V> {
|
||||||
subscription
|
subscription
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Subscribe to events emitted by another model or view.
|
||||||
|
/// The entity to which you're subscribing must implement the [`EventEmitter`] trait.
|
||||||
|
/// The callback will be invoked with a reference to the current view, a handle to the emitting entity (either a [`View`] or [`Model`]), the event, and a view context for the current view.
|
||||||
pub fn subscribe<V2, E, Evt>(
|
pub fn subscribe<V2, E, Evt>(
|
||||||
&mut self,
|
&mut self,
|
||||||
entity: &E,
|
entity: &E,
|
||||||
|
@ -2440,6 +2514,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
|
||||||
subscription
|
subscription
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Register a callback to be invoked when the given Model or View is released.
|
||||||
pub fn observe_release<V2, E>(
|
pub fn observe_release<V2, E>(
|
||||||
&mut self,
|
&mut self,
|
||||||
entity: &E,
|
entity: &E,
|
||||||
|
@ -2466,6 +2541,8 @@ impl<'a, V: 'static> ViewContext<'a, V> {
|
||||||
subscription
|
subscription
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Indicate that this view has changed, which will invoke any observers and also mark the window as dirty.
|
||||||
|
/// If this view or any of its ancestors are *cached*, notifying it will cause it or its ancestors to be redrawn.
|
||||||
pub fn notify(&mut self) {
|
pub fn notify(&mut self) {
|
||||||
if !self.window.drawing {
|
if !self.window.drawing {
|
||||||
self.window_cx.notify();
|
self.window_cx.notify();
|
||||||
|
@ -2475,6 +2552,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Register a callback to be invoked when the window is resized.
|
||||||
pub fn observe_window_bounds(
|
pub fn observe_window_bounds(
|
||||||
&mut self,
|
&mut self,
|
||||||
mut callback: impl FnMut(&mut V, &mut ViewContext<V>) + 'static,
|
mut callback: impl FnMut(&mut V, &mut ViewContext<V>) + 'static,
|
||||||
|
@ -2488,6 +2566,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
|
||||||
subscription
|
subscription
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Register a callback to be invoked when the window is activated or deactivated.
|
||||||
pub fn observe_window_activation(
|
pub fn observe_window_activation(
|
||||||
&mut self,
|
&mut self,
|
||||||
mut callback: impl FnMut(&mut V, &mut ViewContext<V>) + 'static,
|
mut callback: impl FnMut(&mut V, &mut ViewContext<V>) + 'static,
|
||||||
|
@ -2579,14 +2658,16 @@ impl<'a, V: 'static> ViewContext<'a, V> {
|
||||||
subscription
|
subscription
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Register a listener to be called when the window loses focus.
|
/// Register a listener to be called when nothing in the window has focus.
|
||||||
|
/// This typically happens when the node that was focused is removed from the tree,
|
||||||
|
/// and this callback lets you chose a default place to restore the users focus.
|
||||||
/// Returns a subscription and persists until the subscription is dropped.
|
/// Returns a subscription and persists until the subscription is dropped.
|
||||||
pub fn on_blur_window(
|
pub fn on_focus_lost(
|
||||||
&mut self,
|
&mut self,
|
||||||
mut listener: impl FnMut(&mut V, &mut ViewContext<V>) + 'static,
|
mut listener: impl FnMut(&mut V, &mut ViewContext<V>) + 'static,
|
||||||
) -> Subscription {
|
) -> Subscription {
|
||||||
let view = self.view.downgrade();
|
let view = self.view.downgrade();
|
||||||
let (subscription, activate) = self.window.blur_listeners.insert(
|
let (subscription, activate) = self.window.focus_lost_listeners.insert(
|
||||||
(),
|
(),
|
||||||
Box::new(move |cx| view.update(cx, |view, cx| listener(view, cx)).is_ok()),
|
Box::new(move |cx| view.update(cx, |view, cx| listener(view, cx)).is_ok()),
|
||||||
);
|
);
|
||||||
|
@ -2620,6 +2701,10 @@ impl<'a, V: 'static> ViewContext<'a, V> {
|
||||||
subscription
|
subscription
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Schedule a future to be run asynchronously.
|
||||||
|
/// The given callback is invoked with a [`WeakView<V>`] to avoid leaking the view for a long-running process.
|
||||||
|
/// It's also given an [`AsyncWindowContext`], which can be used to access the state of the view across await points.
|
||||||
|
/// The returned future will be polled on the main thread.
|
||||||
pub fn spawn<Fut, R>(
|
pub fn spawn<Fut, R>(
|
||||||
&mut self,
|
&mut self,
|
||||||
f: impl FnOnce(WeakView<V>, AsyncWindowContext) -> Fut,
|
f: impl FnOnce(WeakView<V>, AsyncWindowContext) -> Fut,
|
||||||
|
@ -2632,6 +2717,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
|
||||||
self.window_cx.spawn(|cx| f(view, cx))
|
self.window_cx.spawn(|cx| f(view, cx))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Update the global state of the given type.
|
||||||
pub fn update_global<G, R>(&mut self, f: impl FnOnce(&mut G, &mut Self) -> R) -> R
|
pub fn update_global<G, R>(&mut self, f: impl FnOnce(&mut G, &mut Self) -> R) -> R
|
||||||
where
|
where
|
||||||
G: 'static,
|
G: 'static,
|
||||||
|
@ -2642,6 +2728,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Register a callback to be invoked when the given global state changes.
|
||||||
pub fn observe_global<G: 'static>(
|
pub fn observe_global<G: 'static>(
|
||||||
&mut self,
|
&mut self,
|
||||||
mut f: impl FnMut(&mut V, &mut ViewContext<'_, V>) + 'static,
|
mut f: impl FnMut(&mut V, &mut ViewContext<'_, V>) + 'static,
|
||||||
|
@ -2660,6 +2747,9 @@ impl<'a, V: 'static> ViewContext<'a, V> {
|
||||||
subscription
|
subscription
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Add a listener for any mouse event that occurs in the window.
|
||||||
|
/// This is a fairly low level method.
|
||||||
|
/// Typically, you'll want to use methods on UI elements, which perform bounds checking etc.
|
||||||
pub fn on_mouse_event<Event: 'static>(
|
pub fn on_mouse_event<Event: 'static>(
|
||||||
&mut self,
|
&mut self,
|
||||||
handler: impl Fn(&mut V, &Event, DispatchPhase, &mut ViewContext<V>) + 'static,
|
handler: impl Fn(&mut V, &Event, DispatchPhase, &mut ViewContext<V>) + 'static,
|
||||||
|
@ -2672,6 +2762,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Register a callback to be invoked when the given Key Event is dispatched to the window.
|
||||||
pub fn on_key_event<Event: 'static>(
|
pub fn on_key_event<Event: 'static>(
|
||||||
&mut self,
|
&mut self,
|
||||||
handler: impl Fn(&mut V, &Event, DispatchPhase, &mut ViewContext<V>) + 'static,
|
handler: impl Fn(&mut V, &Event, DispatchPhase, &mut ViewContext<V>) + 'static,
|
||||||
|
@ -2684,6 +2775,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Register a callback to be invoked when the given Action type is dispatched to the window.
|
||||||
pub fn on_action(
|
pub fn on_action(
|
||||||
&mut self,
|
&mut self,
|
||||||
action_type: TypeId,
|
action_type: TypeId,
|
||||||
|
@ -2698,6 +2790,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Emit an event to be handled any other views that have subscribed via [ViewContext::subscribe].
|
||||||
pub fn emit<Evt>(&mut self, event: Evt)
|
pub fn emit<Evt>(&mut self, event: Evt)
|
||||||
where
|
where
|
||||||
Evt: 'static,
|
Evt: 'static,
|
||||||
|
@ -2711,6 +2804,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Move focus to the current view, assuming it implements [`FocusableView`].
|
||||||
pub fn focus_self(&mut self)
|
pub fn focus_self(&mut self)
|
||||||
where
|
where
|
||||||
V: FocusableView,
|
V: FocusableView,
|
||||||
|
@ -2718,6 +2812,11 @@ impl<'a, V: 'static> ViewContext<'a, V> {
|
||||||
self.defer(|view, cx| view.focus_handle(cx).focus(cx))
|
self.defer(|view, cx| view.focus_handle(cx).focus(cx))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Convenience method for accessing view state in an event callback.
|
||||||
|
///
|
||||||
|
/// Many GPUI callbacks take the form of `Fn(&E, &mut WindowContext)`,
|
||||||
|
/// but it's often useful to be able to access view state in these
|
||||||
|
/// callbacks. This method provides a convenient way to do so.
|
||||||
pub fn listener<E>(
|
pub fn listener<E>(
|
||||||
&self,
|
&self,
|
||||||
f: impl Fn(&mut V, &E, &mut ViewContext<V>) + 'static,
|
f: impl Fn(&mut V, &E, &mut ViewContext<V>) + 'static,
|
||||||
|
@ -2827,14 +2926,20 @@ impl<'a, V> std::ops::DerefMut for ViewContext<'a, V> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// #[derive(Clone, Copy, Eq, PartialEq, Hash)]
|
// #[derive(Clone, Copy, Eq, PartialEq, Hash)]
|
||||||
slotmap::new_key_type! { pub struct WindowId; }
|
slotmap::new_key_type! {
|
||||||
|
/// A unique identifier for a window.
|
||||||
|
pub struct WindowId;
|
||||||
|
}
|
||||||
|
|
||||||
impl WindowId {
|
impl WindowId {
|
||||||
|
/// Converts this window ID to a `u64`.
|
||||||
pub fn as_u64(&self) -> u64 {
|
pub fn as_u64(&self) -> u64 {
|
||||||
self.0.as_ffi()
|
self.0.as_ffi()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A handle to a window with a specific root view type.
|
||||||
|
/// Note that this does not keep the window alive on its own.
|
||||||
#[derive(Deref, DerefMut)]
|
#[derive(Deref, DerefMut)]
|
||||||
pub struct WindowHandle<V> {
|
pub struct WindowHandle<V> {
|
||||||
#[deref]
|
#[deref]
|
||||||
|
@ -2844,6 +2949,8 @@ pub struct WindowHandle<V> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<V: 'static + Render> WindowHandle<V> {
|
impl<V: 'static + Render> WindowHandle<V> {
|
||||||
|
/// Create a new handle from a window ID.
|
||||||
|
/// This does not check if the root type of the window is `V`.
|
||||||
pub fn new(id: WindowId) -> Self {
|
pub fn new(id: WindowId) -> Self {
|
||||||
WindowHandle {
|
WindowHandle {
|
||||||
any_handle: AnyWindowHandle {
|
any_handle: AnyWindowHandle {
|
||||||
|
@ -2854,6 +2961,9 @@ impl<V: 'static + Render> WindowHandle<V> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the root view out of this window.
|
||||||
|
///
|
||||||
|
/// This will fail if the window is closed or if the root view's type does not match `V`.
|
||||||
pub fn root<C>(&self, cx: &mut C) -> Result<View<V>>
|
pub fn root<C>(&self, cx: &mut C) -> Result<View<V>>
|
||||||
where
|
where
|
||||||
C: Context,
|
C: Context,
|
||||||
|
@ -2865,6 +2975,9 @@ impl<V: 'static + Render> WindowHandle<V> {
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Update the root view of this window.
|
||||||
|
///
|
||||||
|
/// This will fail if the window has been closed or if the root view's type does not match
|
||||||
pub fn update<C, R>(
|
pub fn update<C, R>(
|
||||||
&self,
|
&self,
|
||||||
cx: &mut C,
|
cx: &mut C,
|
||||||
|
@ -2881,6 +2994,9 @@ impl<V: 'static + Render> WindowHandle<V> {
|
||||||
})?
|
})?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Read the root view out of this window.
|
||||||
|
///
|
||||||
|
/// This will fail if the window is closed or if the root view's type does not match `V`.
|
||||||
pub fn read<'a>(&self, cx: &'a AppContext) -> Result<&'a V> {
|
pub fn read<'a>(&self, cx: &'a AppContext) -> Result<&'a V> {
|
||||||
let x = cx
|
let x = cx
|
||||||
.windows
|
.windows
|
||||||
|
@ -2897,6 +3013,9 @@ impl<V: 'static + Render> WindowHandle<V> {
|
||||||
Ok(x.read(cx))
|
Ok(x.read(cx))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Read the root view out of this window, with a callback
|
||||||
|
///
|
||||||
|
/// This will fail if the window is closed or if the root view's type does not match `V`.
|
||||||
pub fn read_with<C, R>(&self, cx: &C, read_with: impl FnOnce(&V, &AppContext) -> R) -> Result<R>
|
pub fn read_with<C, R>(&self, cx: &C, read_with: impl FnOnce(&V, &AppContext) -> R) -> Result<R>
|
||||||
where
|
where
|
||||||
C: Context,
|
C: Context,
|
||||||
|
@ -2904,6 +3023,9 @@ impl<V: 'static + Render> WindowHandle<V> {
|
||||||
cx.read_window(self, |root_view, cx| read_with(root_view.read(cx), cx))
|
cx.read_window(self, |root_view, cx| read_with(root_view.read(cx), cx))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Read the root view pointer off of this window.
|
||||||
|
///
|
||||||
|
/// This will fail if the window is closed or if the root view's type does not match `V`.
|
||||||
pub fn root_view<C>(&self, cx: &C) -> Result<View<V>>
|
pub fn root_view<C>(&self, cx: &C) -> Result<View<V>>
|
||||||
where
|
where
|
||||||
C: Context,
|
C: Context,
|
||||||
|
@ -2911,6 +3033,9 @@ impl<V: 'static + Render> WindowHandle<V> {
|
||||||
cx.read_window(self, |root_view, _cx| root_view.clone())
|
cx.read_window(self, |root_view, _cx| root_view.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check if this window is 'active'.
|
||||||
|
///
|
||||||
|
/// Will return `None` if the window is closed.
|
||||||
pub fn is_active(&self, cx: &AppContext) -> Option<bool> {
|
pub fn is_active(&self, cx: &AppContext) -> Option<bool> {
|
||||||
cx.windows
|
cx.windows
|
||||||
.get(self.id)
|
.get(self.id)
|
||||||
|
@ -2946,6 +3071,7 @@ impl<V: 'static> From<WindowHandle<V>> for AnyWindowHandle {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A handle to a window with any root view type, which can be downcast to a window with a specific root view type.
|
||||||
#[derive(Copy, Clone, PartialEq, Eq, Hash)]
|
#[derive(Copy, Clone, PartialEq, Eq, Hash)]
|
||||||
pub struct AnyWindowHandle {
|
pub struct AnyWindowHandle {
|
||||||
pub(crate) id: WindowId,
|
pub(crate) id: WindowId,
|
||||||
|
@ -2953,10 +3079,13 @@ pub struct AnyWindowHandle {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AnyWindowHandle {
|
impl AnyWindowHandle {
|
||||||
|
/// Get the ID of this window.
|
||||||
pub fn window_id(&self) -> WindowId {
|
pub fn window_id(&self) -> WindowId {
|
||||||
self.id
|
self.id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Attempt to convert this handle to a window handle with a specific root view type.
|
||||||
|
/// If the types do not match, this will return `None`.
|
||||||
pub fn downcast<T: 'static>(&self) -> Option<WindowHandle<T>> {
|
pub fn downcast<T: 'static>(&self) -> Option<WindowHandle<T>> {
|
||||||
if TypeId::of::<T>() == self.state_type {
|
if TypeId::of::<T>() == self.state_type {
|
||||||
Some(WindowHandle {
|
Some(WindowHandle {
|
||||||
|
@ -2968,6 +3097,9 @@ impl AnyWindowHandle {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Update the state of the root view of this window.
|
||||||
|
///
|
||||||
|
/// This will fail if the window has been closed.
|
||||||
pub fn update<C, R>(
|
pub fn update<C, R>(
|
||||||
self,
|
self,
|
||||||
cx: &mut C,
|
cx: &mut C,
|
||||||
|
@ -2979,6 +3111,9 @@ impl AnyWindowHandle {
|
||||||
cx.update_window(self, update)
|
cx.update_window(self, update)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Read the state of the root view of this window.
|
||||||
|
///
|
||||||
|
/// This will fail if the window has been closed.
|
||||||
pub fn read<T, C, R>(self, cx: &C, read: impl FnOnce(View<T>, &AppContext) -> R) -> Result<R>
|
pub fn read<T, C, R>(self, cx: &C, read: impl FnOnce(View<T>, &AppContext) -> R) -> Result<R>
|
||||||
where
|
where
|
||||||
C: Context,
|
C: Context,
|
||||||
|
@ -2999,12 +3134,21 @@ impl AnyWindowHandle {
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
/// An identifier for an [`Element`](crate::Element).
|
||||||
|
///
|
||||||
|
/// Can be constructed with a string, a number, or both, as well
|
||||||
|
/// as other internal representations.
|
||||||
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
|
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
|
||||||
pub enum ElementId {
|
pub enum ElementId {
|
||||||
|
/// The ID of a View element
|
||||||
View(EntityId),
|
View(EntityId),
|
||||||
|
/// An integer ID.
|
||||||
Integer(usize),
|
Integer(usize),
|
||||||
|
/// A string based ID.
|
||||||
Name(SharedString),
|
Name(SharedString),
|
||||||
|
/// An ID that's equated with a focus handle.
|
||||||
FocusHandle(FocusId),
|
FocusHandle(FocusId),
|
||||||
|
/// A combination of a name and an integer.
|
||||||
NamedInteger(SharedString, usize),
|
NamedInteger(SharedString, usize),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3074,7 +3218,8 @@ impl From<(&'static str, u64)> for ElementId {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A rectangle, to be rendered on the screen by GPUI at the given position and size.
|
/// A rectangle to be rendered in the window at the given position and size.
|
||||||
|
/// Passed as an argument [`WindowContext::paint_quad`].
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct PaintQuad {
|
pub struct PaintQuad {
|
||||||
bounds: Bounds<Pixels>,
|
bounds: Bounds<Pixels>,
|
||||||
|
|
|
@ -18,33 +18,33 @@ fn test_action_macros() {
|
||||||
|
|
||||||
impl gpui::Action for RegisterableAction {
|
impl gpui::Action for RegisterableAction {
|
||||||
fn boxed_clone(&self) -> Box<dyn gpui::Action> {
|
fn boxed_clone(&self) -> Box<dyn gpui::Action> {
|
||||||
todo!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn as_any(&self) -> &dyn std::any::Any {
|
fn as_any(&self) -> &dyn std::any::Any {
|
||||||
todo!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn partial_eq(&self, _action: &dyn gpui::Action) -> bool {
|
fn partial_eq(&self, _action: &dyn gpui::Action) -> bool {
|
||||||
todo!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn name(&self) -> &str {
|
fn name(&self) -> &str {
|
||||||
todo!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn debug_name() -> &'static str
|
fn debug_name() -> &'static str
|
||||||
where
|
where
|
||||||
Self: Sized,
|
Self: Sized,
|
||||||
{
|
{
|
||||||
todo!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build(_value: serde_json::Value) -> anyhow::Result<Box<dyn gpui::Action>>
|
fn build(_value: serde_json::Value) -> anyhow::Result<Box<dyn gpui::Action>>
|
||||||
where
|
where
|
||||||
Self: Sized,
|
Self: Sized,
|
||||||
{
|
{
|
||||||
todo!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,26 +7,56 @@ mod test;
|
||||||
use proc_macro::TokenStream;
|
use proc_macro::TokenStream;
|
||||||
|
|
||||||
#[proc_macro]
|
#[proc_macro]
|
||||||
|
/// register_action! can be used to register an action with the GPUI runtime.
|
||||||
|
/// You should typically use `gpui::actions!` or `gpui::impl_actions!` instead,
|
||||||
|
/// but this can be used for fine grained customization.
|
||||||
pub fn register_action(ident: TokenStream) -> TokenStream {
|
pub fn register_action(ident: TokenStream) -> TokenStream {
|
||||||
register_action::register_action_macro(ident)
|
register_action::register_action_macro(ident)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[proc_macro_derive(IntoElement)]
|
#[proc_macro_derive(IntoElement)]
|
||||||
|
// #[derive(IntoElement)] is used to create a Component out of anything that implements
|
||||||
|
// the `RenderOnce` trait.
|
||||||
pub fn derive_into_element(input: TokenStream) -> TokenStream {
|
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)]
|
#[proc_macro_derive(Render)]
|
||||||
|
#[doc(hidden)]
|
||||||
pub fn derive_render(input: TokenStream) -> TokenStream {
|
pub fn derive_render(input: TokenStream) -> TokenStream {
|
||||||
derive_render::derive_render(input)
|
derive_render::derive_render(input)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Used by gpui to generate the style helpers.
|
||||||
#[proc_macro]
|
#[proc_macro]
|
||||||
|
#[doc(hidden)]
|
||||||
pub fn style_helpers(input: TokenStream) -> TokenStream {
|
pub fn style_helpers(input: TokenStream) -> TokenStream {
|
||||||
style_helpers::style_helpers(input)
|
style_helpers::style_helpers(input)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[proc_macro_attribute]
|
#[proc_macro_attribute]
|
||||||
|
/// #[gpui::test] can be used to annotate test functions that run with GPUI support.
|
||||||
|
/// it supports both synchronous and asynchronous tests, and can provide you with
|
||||||
|
/// as many `TestAppContext` instances as you need.
|
||||||
|
/// The output contains a `#[test]` annotation so this can be used with any existing
|
||||||
|
/// test harness (`cargo test` or `cargo-nextest`).
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// #[gpui::test]
|
||||||
|
/// async fn test_foo(mut cx: &TestAppContext) { }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// In addition to passing a TestAppContext, you can also ask for a `StdRnd` instance.
|
||||||
|
/// this will be seeded with the `SEED` environment variable and is used internally by
|
||||||
|
/// the ForegroundExecutor and BackgroundExecutor to run tasks deterministically in tests.
|
||||||
|
/// Using the same `StdRng` for behaviour in your test will allow you to exercise a wide
|
||||||
|
/// variety of scenarios and interleavings just by changing the seed.
|
||||||
|
///
|
||||||
|
/// #[gpui::test] also takes three different arguments:
|
||||||
|
/// - `#[gpui::test(interations=10)]` will run the test ten times with a different initial SEED.
|
||||||
|
/// - `#[gpui::test(retries=3)]` will run the test up to four times if it fails to try and make it pass.
|
||||||
|
/// - `#[gpui::test(on_failure="crate::test::report_failure")]` will call the specified function after the
|
||||||
|
/// tests fail so that you can write out more detail about the failure.
|
||||||
pub fn test(args: TokenStream, function: TokenStream) -> TokenStream {
|
pub fn test(args: TokenStream, function: TokenStream) -> TokenStream {
|
||||||
test::test(args, function)
|
test::test(args, function)
|
||||||
}
|
}
|
||||||
|
|
|
@ -106,7 +106,8 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream {
|
||||||
let cx_varname = format_ident!("cx_{}", ix);
|
let cx_varname = format_ident!("cx_{}", ix);
|
||||||
cx_vars.extend(quote!(
|
cx_vars.extend(quote!(
|
||||||
let mut #cx_varname = gpui::TestAppContext::new(
|
let mut #cx_varname = gpui::TestAppContext::new(
|
||||||
dispatcher.clone()
|
dispatcher.clone(),
|
||||||
|
Some(stringify!(#outer_fn_name)),
|
||||||
);
|
);
|
||||||
));
|
));
|
||||||
cx_teardowns.extend(quote!(
|
cx_teardowns.extend(quote!(
|
||||||
|
@ -140,8 +141,7 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream {
|
||||||
executor.block_test(#inner_fn_name(#inner_fn_args));
|
executor.block_test(#inner_fn_name(#inner_fn_args));
|
||||||
#cx_teardowns
|
#cx_teardowns
|
||||||
},
|
},
|
||||||
#on_failure_fn_name,
|
#on_failure_fn_name
|
||||||
stringify!(#outer_fn_name).to_string(),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -169,7 +169,8 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream {
|
||||||
let cx_varname_lock = format_ident!("cx_{}_lock", ix);
|
let cx_varname_lock = format_ident!("cx_{}_lock", ix);
|
||||||
cx_vars.extend(quote!(
|
cx_vars.extend(quote!(
|
||||||
let mut #cx_varname = gpui::TestAppContext::new(
|
let mut #cx_varname = gpui::TestAppContext::new(
|
||||||
dispatcher.clone()
|
dispatcher.clone(),
|
||||||
|
Some(stringify!(#outer_fn_name))
|
||||||
);
|
);
|
||||||
let mut #cx_varname_lock = #cx_varname.app.borrow_mut();
|
let mut #cx_varname_lock = #cx_varname.app.borrow_mut();
|
||||||
));
|
));
|
||||||
|
@ -186,7 +187,8 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream {
|
||||||
let cx_varname = format_ident!("cx_{}", ix);
|
let cx_varname = format_ident!("cx_{}", ix);
|
||||||
cx_vars.extend(quote!(
|
cx_vars.extend(quote!(
|
||||||
let mut #cx_varname = gpui::TestAppContext::new(
|
let mut #cx_varname = gpui::TestAppContext::new(
|
||||||
dispatcher.clone()
|
dispatcher.clone(),
|
||||||
|
Some(stringify!(#outer_fn_name))
|
||||||
);
|
);
|
||||||
));
|
));
|
||||||
cx_teardowns.extend(quote!(
|
cx_teardowns.extend(quote!(
|
||||||
|
@ -222,7 +224,6 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream {
|
||||||
#cx_teardowns
|
#cx_teardowns
|
||||||
},
|
},
|
||||||
#on_failure_fn_name,
|
#on_failure_fn_name,
|
||||||
stringify!(#outer_fn_name).to_string(),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -254,6 +254,7 @@ pub enum Event {
|
||||||
LanguageChanged,
|
LanguageChanged,
|
||||||
Reparsed,
|
Reparsed,
|
||||||
DiagnosticsUpdated,
|
DiagnosticsUpdated,
|
||||||
|
CapabilityChanged,
|
||||||
Closed,
|
Closed,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -631,6 +632,11 @@ impl Buffer {
|
||||||
.set_language_registry(language_registry);
|
.set_language_registry(language_registry);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_capability(&mut self, capability: Capability, cx: &mut ModelContext<Self>) {
|
||||||
|
self.capability = capability;
|
||||||
|
cx.emit(Event::CapabilityChanged)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn did_save(
|
pub fn did_save(
|
||||||
&mut self,
|
&mut self,
|
||||||
version: clock::Global,
|
version: clock::Global,
|
||||||
|
|
|
@ -258,19 +258,19 @@ fn test_typing_multiple_new_injections() {
|
||||||
let (buffer, syntax_map) = test_edit_sequence(
|
let (buffer, syntax_map) = test_edit_sequence(
|
||||||
"Rust",
|
"Rust",
|
||||||
&[
|
&[
|
||||||
"fn a() { dbg }",
|
"fn a() { test_macro }",
|
||||||
"fn a() { dbg«!» }",
|
"fn a() { test_macro«!» }",
|
||||||
"fn a() { dbg!«()» }",
|
"fn a() { test_macro!«()» }",
|
||||||
"fn a() { dbg!(«b») }",
|
"fn a() { test_macro!(«b») }",
|
||||||
"fn a() { dbg!(b«.») }",
|
"fn a() { test_macro!(b«.») }",
|
||||||
"fn a() { dbg!(b.«c») }",
|
"fn a() { test_macro!(b.«c») }",
|
||||||
"fn a() { dbg!(b.c«()») }",
|
"fn a() { test_macro!(b.c«()») }",
|
||||||
"fn a() { dbg!(b.c(«vec»)) }",
|
"fn a() { test_macro!(b.c(«vec»)) }",
|
||||||
"fn a() { dbg!(b.c(vec«!»)) }",
|
"fn a() { test_macro!(b.c(vec«!»)) }",
|
||||||
"fn a() { dbg!(b.c(vec!«[]»)) }",
|
"fn a() { test_macro!(b.c(vec!«[]»)) }",
|
||||||
"fn a() { dbg!(b.c(vec![«d»])) }",
|
"fn a() { test_macro!(b.c(vec![«d»])) }",
|
||||||
"fn a() { dbg!(b.c(vec![d«.»])) }",
|
"fn a() { test_macro!(b.c(vec![d«.»])) }",
|
||||||
"fn a() { dbg!(b.c(vec![d.«e»])) }",
|
"fn a() { test_macro!(b.c(vec![d.«e»])) }",
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -278,7 +278,7 @@ fn test_typing_multiple_new_injections() {
|
||||||
&syntax_map,
|
&syntax_map,
|
||||||
&buffer,
|
&buffer,
|
||||||
&["field"],
|
&["field"],
|
||||||
"fn a() { dbg!(b.«c»(vec![d.«e»])) }",
|
"fn a() { test_macro!(b.«c»(vec![d.«e»])) }",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,13 +2,12 @@ use editor::{scroll::autoscroll::Autoscroll, Anchor, Editor, ExcerptId};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, canvas, div, rems, uniform_list, AnyElement, AppContext, AvailableSpace, Div,
|
actions, canvas, div, rems, uniform_list, AnyElement, AppContext, AvailableSpace, Div,
|
||||||
EventEmitter, FocusHandle, FocusableView, Hsla, InteractiveElement, IntoElement, Model,
|
EventEmitter, FocusHandle, FocusableView, Hsla, InteractiveElement, IntoElement, Model,
|
||||||
MouseButton, MouseDownEvent, MouseMoveEvent, ParentElement, Pixels, Render, Styled,
|
MouseButton, MouseDownEvent, MouseMoveEvent, ParentElement, Render, Styled,
|
||||||
UniformListScrollHandle, View, ViewContext, VisualContext, WeakView, WindowContext,
|
UniformListScrollHandle, View, ViewContext, VisualContext, WeakView, WindowContext,
|
||||||
};
|
};
|
||||||
use language::{Buffer, OwnedSyntaxLayerInfo};
|
use language::{Buffer, OwnedSyntaxLayerInfo};
|
||||||
use settings::Settings;
|
|
||||||
use std::{mem, ops::Range};
|
use std::{mem, ops::Range};
|
||||||
use theme::{ActiveTheme, ThemeSettings};
|
use theme::ActiveTheme;
|
||||||
use tree_sitter::{Node, TreeCursor};
|
use tree_sitter::{Node, TreeCursor};
|
||||||
use ui::{h_stack, popover_menu, ButtonLike, Color, ContextMenu, Label, LabelCommon, PopoverMenu};
|
use ui::{h_stack, popover_menu, ButtonLike, Color, ContextMenu, Label, LabelCommon, PopoverMenu};
|
||||||
use workspace::{
|
use workspace::{
|
||||||
|
@ -34,8 +33,6 @@ pub fn init(cx: &mut AppContext) {
|
||||||
pub struct SyntaxTreeView {
|
pub struct SyntaxTreeView {
|
||||||
workspace_handle: WeakView<Workspace>,
|
workspace_handle: WeakView<Workspace>,
|
||||||
editor: Option<EditorState>,
|
editor: Option<EditorState>,
|
||||||
mouse_y: Option<Pixels>,
|
|
||||||
line_height: Option<Pixels>,
|
|
||||||
list_scroll_handle: UniformListScrollHandle,
|
list_scroll_handle: UniformListScrollHandle,
|
||||||
selected_descendant_ix: Option<usize>,
|
selected_descendant_ix: Option<usize>,
|
||||||
hovered_descendant_ix: Option<usize>,
|
hovered_descendant_ix: Option<usize>,
|
||||||
|
@ -70,8 +67,6 @@ impl SyntaxTreeView {
|
||||||
workspace_handle: workspace_handle.clone(),
|
workspace_handle: workspace_handle.clone(),
|
||||||
list_scroll_handle: UniformListScrollHandle::new(),
|
list_scroll_handle: UniformListScrollHandle::new(),
|
||||||
editor: None,
|
editor: None,
|
||||||
mouse_y: None,
|
|
||||||
line_height: None,
|
|
||||||
hovered_descendant_ix: None,
|
hovered_descendant_ix: None,
|
||||||
selected_descendant_ix: None,
|
selected_descendant_ix: None,
|
||||||
focus_handle: cx.focus_handle(),
|
focus_handle: cx.focus_handle(),
|
||||||
|
@ -208,39 +203,6 @@ impl SyntaxTreeView {
|
||||||
Some(())
|
Some(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_click(&mut self, y: Pixels, cx: &mut ViewContext<SyntaxTreeView>) -> Option<()> {
|
|
||||||
let line_height = self.line_height?;
|
|
||||||
let ix = ((self.list_scroll_handle.scroll_top() + y) / line_height) as usize;
|
|
||||||
|
|
||||||
self.update_editor_with_range_for_descendant_ix(ix, cx, |editor, mut range, cx| {
|
|
||||||
// Put the cursor at the beginning of the node.
|
|
||||||
mem::swap(&mut range.start, &mut range.end);
|
|
||||||
|
|
||||||
editor.change_selections(Some(Autoscroll::newest()), cx, |selections| {
|
|
||||||
selections.select_ranges(vec![range]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Some(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn hover_state_changed(&mut self, cx: &mut ViewContext<SyntaxTreeView>) {
|
|
||||||
if let Some((y, line_height)) = self.mouse_y.zip(self.line_height) {
|
|
||||||
let ix = ((self.list_scroll_handle.scroll_top() + y) / line_height) as usize;
|
|
||||||
if self.hovered_descendant_ix != Some(ix) {
|
|
||||||
self.hovered_descendant_ix = Some(ix);
|
|
||||||
self.update_editor_with_range_for_descendant_ix(ix, cx, |editor, range, cx| {
|
|
||||||
editor.clear_background_highlights::<Self>(cx);
|
|
||||||
editor.highlight_background::<Self>(
|
|
||||||
vec![range],
|
|
||||||
|theme| theme.editor_document_highlight_write_background,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update_editor_with_range_for_descendant_ix(
|
fn update_editor_with_range_for_descendant_ix(
|
||||||
&self,
|
&self,
|
||||||
descendant_ix: usize,
|
descendant_ix: usize,
|
||||||
|
@ -306,15 +268,6 @@ impl SyntaxTreeView {
|
||||||
|
|
||||||
impl Render for SyntaxTreeView {
|
impl Render for SyntaxTreeView {
|
||||||
fn render(&mut self, cx: &mut gpui::ViewContext<'_, Self>) -> impl IntoElement {
|
fn render(&mut self, cx: &mut gpui::ViewContext<'_, Self>) -> impl IntoElement {
|
||||||
let settings = ThemeSettings::get_global(cx);
|
|
||||||
let line_height = cx
|
|
||||||
.text_style()
|
|
||||||
.line_height_in_pixels(settings.buffer_font_size(cx));
|
|
||||||
if Some(line_height) != self.line_height {
|
|
||||||
self.line_height = Some(line_height);
|
|
||||||
self.hover_state_changed(cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut rendered = div().flex_1();
|
let mut rendered = div().flex_1();
|
||||||
|
|
||||||
if let Some(layer) = self
|
if let Some(layer) = self
|
||||||
|
@ -345,12 +298,51 @@ impl Render for SyntaxTreeView {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
items.push(Self::render_node(
|
items.push(
|
||||||
&cursor,
|
Self::render_node(
|
||||||
depth,
|
&cursor,
|
||||||
Some(descendant_ix) == this.selected_descendant_ix,
|
depth,
|
||||||
cx,
|
Some(descendant_ix) == this.selected_descendant_ix,
|
||||||
));
|
cx,
|
||||||
|
)
|
||||||
|
.on_mouse_down(
|
||||||
|
MouseButton::Left,
|
||||||
|
cx.listener(move |tree_view, _: &MouseDownEvent, cx| {
|
||||||
|
tree_view.update_editor_with_range_for_descendant_ix(
|
||||||
|
descendant_ix,
|
||||||
|
cx,
|
||||||
|
|editor, mut range, cx| {
|
||||||
|
// Put the cursor at the beginning of the node.
|
||||||
|
mem::swap(&mut range.start, &mut range.end);
|
||||||
|
|
||||||
|
editor.change_selections(
|
||||||
|
Some(Autoscroll::newest()),
|
||||||
|
cx,
|
||||||
|
|selections| {
|
||||||
|
selections.select_ranges(vec![range]);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.on_mouse_move(cx.listener(
|
||||||
|
move |tree_view, _: &MouseMoveEvent, cx| {
|
||||||
|
if tree_view.hovered_descendant_ix != Some(descendant_ix) {
|
||||||
|
tree_view.hovered_descendant_ix = Some(descendant_ix);
|
||||||
|
tree_view.update_editor_with_range_for_descendant_ix(descendant_ix, cx, |editor, range, cx| {
|
||||||
|
editor.clear_background_highlights::<Self>(cx);
|
||||||
|
editor.highlight_background::<Self>(
|
||||||
|
vec![range],
|
||||||
|
|theme| theme.editor_document_highlight_write_background,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
);
|
||||||
descendant_ix += 1;
|
descendant_ix += 1;
|
||||||
if cursor.goto_first_child() {
|
if cursor.goto_first_child() {
|
||||||
depth += 1;
|
depth += 1;
|
||||||
|
@ -364,16 +356,6 @@ impl Render for SyntaxTreeView {
|
||||||
)
|
)
|
||||||
.size_full()
|
.size_full()
|
||||||
.track_scroll(self.list_scroll_handle.clone())
|
.track_scroll(self.list_scroll_handle.clone())
|
||||||
.on_mouse_move(cx.listener(move |tree_view, event: &MouseMoveEvent, cx| {
|
|
||||||
tree_view.mouse_y = Some(event.position.y);
|
|
||||||
tree_view.hover_state_changed(cx);
|
|
||||||
}))
|
|
||||||
.on_mouse_down(
|
|
||||||
MouseButton::Left,
|
|
||||||
cx.listener(move |tree_view, event: &MouseDownEvent, cx| {
|
|
||||||
tree_view.handle_click(event.position.y, cx);
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.text_bg(cx.theme().colors().background);
|
.text_bg(cx.theme().colors().background);
|
||||||
|
|
||||||
rendered = rendered.child(
|
rendered = rendered.child(
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use std::{sync::Arc, time::Duration};
|
use std::{sync::Arc, time::Duration};
|
||||||
|
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use gpui::{actions, KeyBinding};
|
use gpui::{actions, KeyBinding, Menu, MenuItem};
|
||||||
use live_kit_client::{
|
use live_kit_client::{
|
||||||
LocalAudioTrack, LocalVideoTrack, RemoteAudioTrackUpdate, RemoteVideoTrackUpdate, Room,
|
LocalAudioTrack, LocalVideoTrack, RemoteAudioTrackUpdate, RemoteVideoTrackUpdate, Room,
|
||||||
};
|
};
|
||||||
|
@ -26,15 +26,14 @@ fn main() {
|
||||||
cx.on_action(quit);
|
cx.on_action(quit);
|
||||||
cx.bind_keys([KeyBinding::new("cmd-q", Quit, None)]);
|
cx.bind_keys([KeyBinding::new("cmd-q", Quit, None)]);
|
||||||
|
|
||||||
// todo!()
|
cx.set_menus(vec![Menu {
|
||||||
// cx.set_menus(vec![Menu {
|
name: "Zed",
|
||||||
// name: "Zed",
|
items: vec![MenuItem::Action {
|
||||||
// items: vec![MenuItem::Action {
|
name: "Quit",
|
||||||
// name: "Quit",
|
action: Box::new(Quit),
|
||||||
// action: Box::new(Quit),
|
os_action: None,
|
||||||
// os_action: None,
|
}],
|
||||||
// }],
|
}]);
|
||||||
// }]);
|
|
||||||
|
|
||||||
let live_kit_url = std::env::var("LIVE_KIT_URL").unwrap_or("http://localhost:7880".into());
|
let live_kit_url = std::env::var("LIVE_KIT_URL").unwrap_or("http://localhost:7880".into());
|
||||||
let live_kit_key = std::env::var("LIVE_KIT_KEY").unwrap_or("devkey".into());
|
let live_kit_key = std::env::var("LIVE_KIT_KEY").unwrap_or("devkey".into());
|
||||||
|
|
|
@ -164,29 +164,26 @@ pub enum ConnectionState {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Room {
|
pub struct Room {
|
||||||
native_room: Mutex<swift::Room>,
|
native_room: swift::Room,
|
||||||
connection: Mutex<(
|
connection: Mutex<(
|
||||||
watch::Sender<ConnectionState>,
|
watch::Sender<ConnectionState>,
|
||||||
watch::Receiver<ConnectionState>,
|
watch::Receiver<ConnectionState>,
|
||||||
)>,
|
)>,
|
||||||
remote_audio_track_subscribers: Mutex<Vec<mpsc::UnboundedSender<RemoteAudioTrackUpdate>>>,
|
remote_audio_track_subscribers: Mutex<Vec<mpsc::UnboundedSender<RemoteAudioTrackUpdate>>>,
|
||||||
remote_video_track_subscribers: Mutex<Vec<mpsc::UnboundedSender<RemoteVideoTrackUpdate>>>,
|
remote_video_track_subscribers: Mutex<Vec<mpsc::UnboundedSender<RemoteVideoTrackUpdate>>>,
|
||||||
_delegate: Mutex<RoomDelegate>,
|
_delegate: RoomDelegate,
|
||||||
}
|
}
|
||||||
|
|
||||||
trait AssertSendSync: Send {}
|
|
||||||
impl AssertSendSync for Room {}
|
|
||||||
|
|
||||||
impl Room {
|
impl Room {
|
||||||
pub fn new() -> Arc<Self> {
|
pub fn new() -> Arc<Self> {
|
||||||
Arc::new_cyclic(|weak_room| {
|
Arc::new_cyclic(|weak_room| {
|
||||||
let delegate = RoomDelegate::new(weak_room.clone());
|
let delegate = RoomDelegate::new(weak_room.clone());
|
||||||
Self {
|
Self {
|
||||||
native_room: Mutex::new(unsafe { LKRoomCreate(delegate.native_delegate) }),
|
native_room: unsafe { LKRoomCreate(delegate.native_delegate) },
|
||||||
connection: Mutex::new(watch::channel_with(ConnectionState::Disconnected)),
|
connection: Mutex::new(watch::channel_with(ConnectionState::Disconnected)),
|
||||||
remote_audio_track_subscribers: Default::default(),
|
remote_audio_track_subscribers: Default::default(),
|
||||||
remote_video_track_subscribers: Default::default(),
|
remote_video_track_subscribers: Default::default(),
|
||||||
_delegate: Mutex::new(delegate),
|
_delegate: delegate,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -201,7 +198,7 @@ impl Room {
|
||||||
let (did_connect, tx, rx) = Self::build_done_callback();
|
let (did_connect, tx, rx) = Self::build_done_callback();
|
||||||
unsafe {
|
unsafe {
|
||||||
LKRoomConnect(
|
LKRoomConnect(
|
||||||
*self.native_room.lock(),
|
self.native_room,
|
||||||
url.as_concrete_TypeRef(),
|
url.as_concrete_TypeRef(),
|
||||||
token.as_concrete_TypeRef(),
|
token.as_concrete_TypeRef(),
|
||||||
did_connect,
|
did_connect,
|
||||||
|
@ -271,7 +268,7 @@ impl Room {
|
||||||
}
|
}
|
||||||
unsafe {
|
unsafe {
|
||||||
LKRoomPublishVideoTrack(
|
LKRoomPublishVideoTrack(
|
||||||
*self.native_room.lock(),
|
self.native_room,
|
||||||
track.0,
|
track.0,
|
||||||
callback,
|
callback,
|
||||||
Box::into_raw(Box::new(tx)) as *mut c_void,
|
Box::into_raw(Box::new(tx)) as *mut c_void,
|
||||||
|
@ -301,7 +298,7 @@ impl Room {
|
||||||
}
|
}
|
||||||
unsafe {
|
unsafe {
|
||||||
LKRoomPublishAudioTrack(
|
LKRoomPublishAudioTrack(
|
||||||
*self.native_room.lock(),
|
self.native_room,
|
||||||
track.0,
|
track.0,
|
||||||
callback,
|
callback,
|
||||||
Box::into_raw(Box::new(tx)) as *mut c_void,
|
Box::into_raw(Box::new(tx)) as *mut c_void,
|
||||||
|
@ -312,14 +309,14 @@ impl Room {
|
||||||
|
|
||||||
pub fn unpublish_track(&self, publication: LocalTrackPublication) {
|
pub fn unpublish_track(&self, publication: LocalTrackPublication) {
|
||||||
unsafe {
|
unsafe {
|
||||||
LKRoomUnpublishTrack(*self.native_room.lock(), publication.0);
|
LKRoomUnpublishTrack(self.native_room, publication.0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remote_video_tracks(&self, participant_id: &str) -> Vec<Arc<RemoteVideoTrack>> {
|
pub fn remote_video_tracks(&self, participant_id: &str) -> Vec<Arc<RemoteVideoTrack>> {
|
||||||
unsafe {
|
unsafe {
|
||||||
let tracks = LKRoomVideoTracksForRemoteParticipant(
|
let tracks = LKRoomVideoTracksForRemoteParticipant(
|
||||||
*self.native_room.lock(),
|
self.native_room,
|
||||||
CFString::new(participant_id).as_concrete_TypeRef(),
|
CFString::new(participant_id).as_concrete_TypeRef(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -348,7 +345,7 @@ impl Room {
|
||||||
pub fn remote_audio_tracks(&self, participant_id: &str) -> Vec<Arc<RemoteAudioTrack>> {
|
pub fn remote_audio_tracks(&self, participant_id: &str) -> Vec<Arc<RemoteAudioTrack>> {
|
||||||
unsafe {
|
unsafe {
|
||||||
let tracks = LKRoomAudioTracksForRemoteParticipant(
|
let tracks = LKRoomAudioTracksForRemoteParticipant(
|
||||||
*self.native_room.lock(),
|
self.native_room,
|
||||||
CFString::new(participant_id).as_concrete_TypeRef(),
|
CFString::new(participant_id).as_concrete_TypeRef(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -380,7 +377,7 @@ impl Room {
|
||||||
) -> Vec<Arc<RemoteTrackPublication>> {
|
) -> Vec<Arc<RemoteTrackPublication>> {
|
||||||
unsafe {
|
unsafe {
|
||||||
let tracks = LKRoomAudioTrackPublicationsForRemoteParticipant(
|
let tracks = LKRoomAudioTrackPublicationsForRemoteParticipant(
|
||||||
*self.native_room.lock(),
|
self.native_room,
|
||||||
CFString::new(participant_id).as_concrete_TypeRef(),
|
CFString::new(participant_id).as_concrete_TypeRef(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -508,23 +505,23 @@ impl Room {
|
||||||
impl Drop for Room {
|
impl Drop for Room {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
unsafe {
|
unsafe {
|
||||||
let native_room = &*self.native_room.lock();
|
LKRoomDisconnect(self.native_room);
|
||||||
LKRoomDisconnect(*native_room);
|
CFRelease(self.native_room.0);
|
||||||
CFRelease(native_room.0);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct RoomDelegate {
|
struct RoomDelegate {
|
||||||
native_delegate: swift::RoomDelegate,
|
native_delegate: swift::RoomDelegate,
|
||||||
_weak_room: Weak<Room>,
|
weak_room: *mut c_void,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RoomDelegate {
|
impl RoomDelegate {
|
||||||
fn new(weak_room: Weak<Room>) -> Self {
|
fn new(weak_room: Weak<Room>) -> Self {
|
||||||
|
let weak_room = weak_room.into_raw() as *mut c_void;
|
||||||
let native_delegate = unsafe {
|
let native_delegate = unsafe {
|
||||||
LKRoomDelegateCreate(
|
LKRoomDelegateCreate(
|
||||||
weak_room.as_ptr() as *mut c_void,
|
weak_room,
|
||||||
Self::on_did_disconnect,
|
Self::on_did_disconnect,
|
||||||
Self::on_did_subscribe_to_remote_audio_track,
|
Self::on_did_subscribe_to_remote_audio_track,
|
||||||
Self::on_did_unsubscribe_from_remote_audio_track,
|
Self::on_did_unsubscribe_from_remote_audio_track,
|
||||||
|
@ -536,7 +533,7 @@ impl RoomDelegate {
|
||||||
};
|
};
|
||||||
Self {
|
Self {
|
||||||
native_delegate,
|
native_delegate,
|
||||||
_weak_room: weak_room,
|
weak_room,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -651,6 +648,7 @@ impl Drop for RoomDelegate {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
unsafe {
|
unsafe {
|
||||||
CFRelease(self.native_delegate.0);
|
CFRelease(self.native_delegate.0);
|
||||||
|
let _ = Weak::from_raw(self.weak_room);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -725,31 +723,22 @@ impl Drop for LocalTrackPublication {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct RemoteTrackPublication {
|
pub struct RemoteTrackPublication(swift::RemoteTrackPublication);
|
||||||
native_publication: Mutex<swift::RemoteTrackPublication>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RemoteTrackPublication {
|
impl RemoteTrackPublication {
|
||||||
pub fn new(native_track_publication: swift::RemoteTrackPublication) -> Self {
|
pub fn new(native_track_publication: swift::RemoteTrackPublication) -> Self {
|
||||||
unsafe {
|
unsafe {
|
||||||
CFRetain(native_track_publication.0);
|
CFRetain(native_track_publication.0);
|
||||||
}
|
}
|
||||||
Self {
|
Self(native_track_publication)
|
||||||
native_publication: Mutex::new(native_track_publication),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn sid(&self) -> String {
|
pub fn sid(&self) -> String {
|
||||||
unsafe {
|
unsafe { CFString::wrap_under_get_rule(LKRemoteTrackPublicationGetSid(self.0)).to_string() }
|
||||||
CFString::wrap_under_get_rule(LKRemoteTrackPublicationGetSid(
|
|
||||||
*self.native_publication.lock(),
|
|
||||||
))
|
|
||||||
.to_string()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_muted(&self) -> bool {
|
pub fn is_muted(&self) -> bool {
|
||||||
unsafe { LKRemoteTrackPublicationIsMuted(*self.native_publication.lock()) }
|
unsafe { LKRemoteTrackPublicationIsMuted(self.0) }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_enabled(&self, enabled: bool) -> impl Future<Output = Result<()>> {
|
pub fn set_enabled(&self, enabled: bool) -> impl Future<Output = Result<()>> {
|
||||||
|
@ -767,7 +756,7 @@ impl RemoteTrackPublication {
|
||||||
|
|
||||||
unsafe {
|
unsafe {
|
||||||
LKRemoteTrackPublicationSetEnabled(
|
LKRemoteTrackPublicationSetEnabled(
|
||||||
*self.native_publication.lock(),
|
self.0,
|
||||||
enabled,
|
enabled,
|
||||||
complete_callback,
|
complete_callback,
|
||||||
Box::into_raw(Box::new(tx)) as *mut c_void,
|
Box::into_raw(Box::new(tx)) as *mut c_void,
|
||||||
|
@ -780,13 +769,13 @@ impl RemoteTrackPublication {
|
||||||
|
|
||||||
impl Drop for RemoteTrackPublication {
|
impl Drop for RemoteTrackPublication {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
unsafe { CFRelease((*self.native_publication.lock()).0) }
|
unsafe { CFRelease(self.0 .0) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct RemoteAudioTrack {
|
pub struct RemoteAudioTrack {
|
||||||
native_track: Mutex<swift::RemoteAudioTrack>,
|
native_track: swift::RemoteAudioTrack,
|
||||||
sid: Sid,
|
sid: Sid,
|
||||||
publisher_id: String,
|
publisher_id: String,
|
||||||
}
|
}
|
||||||
|
@ -797,7 +786,7 @@ impl RemoteAudioTrack {
|
||||||
CFRetain(native_track.0);
|
CFRetain(native_track.0);
|
||||||
}
|
}
|
||||||
Self {
|
Self {
|
||||||
native_track: Mutex::new(native_track),
|
native_track,
|
||||||
sid,
|
sid,
|
||||||
publisher_id,
|
publisher_id,
|
||||||
}
|
}
|
||||||
|
@ -822,13 +811,13 @@ impl RemoteAudioTrack {
|
||||||
|
|
||||||
impl Drop for RemoteAudioTrack {
|
impl Drop for RemoteAudioTrack {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
unsafe { CFRelease(self.native_track.lock().0) }
|
unsafe { CFRelease(self.native_track.0) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct RemoteVideoTrack {
|
pub struct RemoteVideoTrack {
|
||||||
native_track: Mutex<swift::RemoteVideoTrack>,
|
native_track: swift::RemoteVideoTrack,
|
||||||
sid: Sid,
|
sid: Sid,
|
||||||
publisher_id: String,
|
publisher_id: String,
|
||||||
}
|
}
|
||||||
|
@ -839,7 +828,7 @@ impl RemoteVideoTrack {
|
||||||
CFRetain(native_track.0);
|
CFRetain(native_track.0);
|
||||||
}
|
}
|
||||||
Self {
|
Self {
|
||||||
native_track: Mutex::new(native_track),
|
native_track,
|
||||||
sid,
|
sid,
|
||||||
publisher_id,
|
publisher_id,
|
||||||
}
|
}
|
||||||
|
@ -888,7 +877,7 @@ impl RemoteVideoTrack {
|
||||||
on_frame,
|
on_frame,
|
||||||
on_drop,
|
on_drop,
|
||||||
);
|
);
|
||||||
LKVideoTrackAddRenderer(*self.native_track.lock(), renderer);
|
LKVideoTrackAddRenderer(self.native_track, renderer);
|
||||||
rx
|
rx
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -896,7 +885,7 @@ impl RemoteVideoTrack {
|
||||||
|
|
||||||
impl Drop for RemoteVideoTrack {
|
impl Drop for RemoteVideoTrack {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
unsafe { CFRelease(self.native_track.lock().0) }
|
unsafe { CFRelease(self.native_track.0) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ use async_trait::async_trait;
|
||||||
use collections::{BTreeMap, HashMap};
|
use collections::{BTreeMap, HashMap};
|
||||||
use futures::Stream;
|
use futures::Stream;
|
||||||
use gpui::BackgroundExecutor;
|
use gpui::BackgroundExecutor;
|
||||||
use live_kit_server::token;
|
use live_kit_server::{proto, token};
|
||||||
use media::core_video::CVImageBuffer;
|
use media::core_video::CVImageBuffer;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use postage::watch;
|
use postage::watch;
|
||||||
|
@ -151,6 +151,21 @@ impl TestServer {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn update_participant(
|
||||||
|
&self,
|
||||||
|
room_name: String,
|
||||||
|
identity: String,
|
||||||
|
permission: proto::ParticipantPermission,
|
||||||
|
) -> Result<()> {
|
||||||
|
self.executor.simulate_random_delay().await;
|
||||||
|
let mut server_rooms = self.rooms.lock();
|
||||||
|
let room = server_rooms
|
||||||
|
.get_mut(&room_name)
|
||||||
|
.ok_or_else(|| anyhow!("room {} does not exist", room_name))?;
|
||||||
|
room.participant_permissions.insert(identity, permission);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn disconnect_client(&self, client_identity: String) {
|
pub async fn disconnect_client(&self, client_identity: String) {
|
||||||
self.executor.simulate_random_delay().await;
|
self.executor.simulate_random_delay().await;
|
||||||
let mut server_rooms = self.rooms.lock();
|
let mut server_rooms = self.rooms.lock();
|
||||||
|
@ -172,6 +187,17 @@ impl TestServer {
|
||||||
.get_mut(&*room_name)
|
.get_mut(&*room_name)
|
||||||
.ok_or_else(|| anyhow!("room {} does not exist", room_name))?;
|
.ok_or_else(|| anyhow!("room {} does not exist", room_name))?;
|
||||||
|
|
||||||
|
let can_publish = room
|
||||||
|
.participant_permissions
|
||||||
|
.get(&identity)
|
||||||
|
.map(|permission| permission.can_publish)
|
||||||
|
.or(claims.video.can_publish)
|
||||||
|
.unwrap_or(true);
|
||||||
|
|
||||||
|
if !can_publish {
|
||||||
|
return Err(anyhow!("user is not allowed to publish"));
|
||||||
|
}
|
||||||
|
|
||||||
let track = Arc::new(RemoteVideoTrack {
|
let track = Arc::new(RemoteVideoTrack {
|
||||||
sid: nanoid::nanoid!(17),
|
sid: nanoid::nanoid!(17),
|
||||||
publisher_id: identity.clone(),
|
publisher_id: identity.clone(),
|
||||||
|
@ -210,6 +236,17 @@ impl TestServer {
|
||||||
.get_mut(&*room_name)
|
.get_mut(&*room_name)
|
||||||
.ok_or_else(|| anyhow!("room {} does not exist", room_name))?;
|
.ok_or_else(|| anyhow!("room {} does not exist", room_name))?;
|
||||||
|
|
||||||
|
let can_publish = room
|
||||||
|
.participant_permissions
|
||||||
|
.get(&identity)
|
||||||
|
.map(|permission| permission.can_publish)
|
||||||
|
.or(claims.video.can_publish)
|
||||||
|
.unwrap_or(true);
|
||||||
|
|
||||||
|
if !can_publish {
|
||||||
|
return Err(anyhow!("user is not allowed to publish"));
|
||||||
|
}
|
||||||
|
|
||||||
let track = Arc::new(RemoteAudioTrack {
|
let track = Arc::new(RemoteAudioTrack {
|
||||||
sid: nanoid::nanoid!(17),
|
sid: nanoid::nanoid!(17),
|
||||||
publisher_id: identity.clone(),
|
publisher_id: identity.clone(),
|
||||||
|
@ -265,6 +302,7 @@ struct TestServerRoom {
|
||||||
client_rooms: HashMap<Sid, Arc<Room>>,
|
client_rooms: HashMap<Sid, Arc<Room>>,
|
||||||
video_tracks: Vec<Arc<RemoteVideoTrack>>,
|
video_tracks: Vec<Arc<RemoteVideoTrack>>,
|
||||||
audio_tracks: Vec<Arc<RemoteAudioTrack>>,
|
audio_tracks: Vec<Arc<RemoteAudioTrack>>,
|
||||||
|
participant_permissions: HashMap<Sid, proto::ParticipantPermission>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TestServerRoom {}
|
impl TestServerRoom {}
|
||||||
|
@ -297,6 +335,19 @@ impl live_kit_server::api::Client for TestApiClient {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn update_participant(
|
||||||
|
&self,
|
||||||
|
room: String,
|
||||||
|
identity: String,
|
||||||
|
permission: live_kit_server::proto::ParticipantPermission,
|
||||||
|
) -> Result<()> {
|
||||||
|
let server = TestServer::get(&self.url)?;
|
||||||
|
server
|
||||||
|
.update_participant(room, identity, permission)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn room_token(&self, room: &str, identity: &str) -> Result<String> {
|
fn room_token(&self, room: &str, identity: &str) -> Result<String> {
|
||||||
let server = TestServer::get(&self.url)?;
|
let server = TestServer::get(&self.url)?;
|
||||||
token::create(
|
token::create(
|
||||||
|
|
|
@ -11,10 +11,18 @@ pub trait Client: Send + Sync {
|
||||||
async fn create_room(&self, name: String) -> Result<()>;
|
async fn create_room(&self, name: String) -> Result<()>;
|
||||||
async fn delete_room(&self, name: String) -> Result<()>;
|
async fn delete_room(&self, name: String) -> Result<()>;
|
||||||
async fn remove_participant(&self, room: String, identity: String) -> Result<()>;
|
async fn remove_participant(&self, room: String, identity: String) -> Result<()>;
|
||||||
|
async fn update_participant(
|
||||||
|
&self,
|
||||||
|
room: String,
|
||||||
|
identity: String,
|
||||||
|
permission: proto::ParticipantPermission,
|
||||||
|
) -> Result<()>;
|
||||||
fn room_token(&self, room: &str, identity: &str) -> Result<String>;
|
fn room_token(&self, room: &str, identity: &str) -> Result<String>;
|
||||||
fn guest_token(&self, room: &str, identity: &str) -> Result<String>;
|
fn guest_token(&self, room: &str, identity: &str) -> Result<String>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct LiveKitParticipantUpdate {}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct LiveKitClient {
|
pub struct LiveKitClient {
|
||||||
http: reqwest::Client,
|
http: reqwest::Client,
|
||||||
|
@ -131,6 +139,27 @@ impl Client for LiveKitClient {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn update_participant(
|
||||||
|
&self,
|
||||||
|
room: String,
|
||||||
|
identity: String,
|
||||||
|
permission: proto::ParticipantPermission,
|
||||||
|
) -> Result<()> {
|
||||||
|
let _: proto::ParticipantInfo = self
|
||||||
|
.request(
|
||||||
|
"twirp/livekit.RoomService/UpdateParticipant",
|
||||||
|
token::VideoGrant::to_admin(&room),
|
||||||
|
proto::UpdateParticipantRequest {
|
||||||
|
room: room.clone(),
|
||||||
|
identity,
|
||||||
|
metadata: "".to_string(),
|
||||||
|
permission: Some(permission),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn room_token(&self, room: &str, identity: &str) -> Result<String> {
|
fn room_token(&self, room: &str, identity: &str) -> Result<String> {
|
||||||
token::create(
|
token::create(
|
||||||
&self.key,
|
&self.key,
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
pub mod api;
|
pub mod api;
|
||||||
mod proto;
|
pub mod proto;
|
||||||
pub mod token;
|
pub mod token;
|
||||||
|
|
|
@ -80,6 +80,7 @@ pub enum Event {
|
||||||
Reloaded,
|
Reloaded,
|
||||||
DiffBaseChanged,
|
DiffBaseChanged,
|
||||||
LanguageChanged,
|
LanguageChanged,
|
||||||
|
CapabilityChanged,
|
||||||
Reparsed,
|
Reparsed,
|
||||||
Saved,
|
Saved,
|
||||||
FileHandleChanged,
|
FileHandleChanged,
|
||||||
|
@ -1404,7 +1405,7 @@ impl MultiBuffer {
|
||||||
|
|
||||||
fn on_buffer_event(
|
fn on_buffer_event(
|
||||||
&mut self,
|
&mut self,
|
||||||
_: Model<Buffer>,
|
buffer: Model<Buffer>,
|
||||||
event: &language::Event,
|
event: &language::Event,
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) {
|
) {
|
||||||
|
@ -1421,6 +1422,10 @@ impl MultiBuffer {
|
||||||
language::Event::Reparsed => Event::Reparsed,
|
language::Event::Reparsed => Event::Reparsed,
|
||||||
language::Event::DiagnosticsUpdated => Event::DiagnosticsUpdated,
|
language::Event::DiagnosticsUpdated => Event::DiagnosticsUpdated,
|
||||||
language::Event::Closed => Event::Closed,
|
language::Event::Closed => Event::Closed,
|
||||||
|
language::Event::CapabilityChanged => {
|
||||||
|
self.capability = buffer.read(cx).capability();
|
||||||
|
Event::CapabilityChanged
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
language::Event::Operation(_) => return,
|
language::Event::Operation(_) => return,
|
||||||
|
|
|
@ -799,7 +799,7 @@ impl Project {
|
||||||
prettiers_per_worktree: HashMap::default(),
|
prettiers_per_worktree: HashMap::default(),
|
||||||
prettier_instances: HashMap::default(),
|
prettier_instances: HashMap::default(),
|
||||||
};
|
};
|
||||||
this.set_role(role);
|
this.set_role(role, cx);
|
||||||
for worktree in worktrees {
|
for worktree in worktrees {
|
||||||
let _ = this.add_worktree(&worktree, cx);
|
let _ = this.add_worktree(&worktree, cx);
|
||||||
}
|
}
|
||||||
|
@ -1622,14 +1622,22 @@ impl Project {
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_role(&mut self, role: proto::ChannelRole) {
|
pub fn set_role(&mut self, role: proto::ChannelRole, cx: &mut ModelContext<Self>) {
|
||||||
if let Some(ProjectClientState::Remote { capability, .. }) = &mut self.client_state {
|
let new_capability =
|
||||||
*capability = if role == proto::ChannelRole::Member || role == proto::ChannelRole::Admin
|
if role == proto::ChannelRole::Member || role == proto::ChannelRole::Admin {
|
||||||
{
|
|
||||||
Capability::ReadWrite
|
Capability::ReadWrite
|
||||||
} else {
|
} else {
|
||||||
Capability::ReadOnly
|
Capability::ReadOnly
|
||||||
};
|
};
|
||||||
|
if let Some(ProjectClientState::Remote { capability, .. }) = &mut self.client_state {
|
||||||
|
if *capability == new_capability {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
*capability = new_capability;
|
||||||
|
}
|
||||||
|
for buffer in self.opened_buffers() {
|
||||||
|
buffer.update(cx, |buffer, cx| buffer.set_capability(new_capability, cx));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4732,7 +4740,8 @@ impl Project {
|
||||||
} else {
|
} else {
|
||||||
return Task::ready(Err(anyhow!("worktree not found for symbol")));
|
return Task::ready(Err(anyhow!("worktree not found for symbol")));
|
||||||
};
|
};
|
||||||
let symbol_abs_path = worktree_abs_path.join(&symbol.path.path);
|
|
||||||
|
let symbol_abs_path = resolve_path(worktree_abs_path, &symbol.path.path);
|
||||||
let symbol_uri = if let Ok(uri) = lsp::Url::from_file_path(symbol_abs_path) {
|
let symbol_uri = if let Ok(uri) = lsp::Url::from_file_path(symbol_abs_path) {
|
||||||
uri
|
uri
|
||||||
} else {
|
} else {
|
||||||
|
@ -6581,7 +6590,14 @@ impl Project {
|
||||||
let removed = *change == PathChange::Removed;
|
let removed = *change == PathChange::Removed;
|
||||||
let abs_path = worktree.absolutize(path);
|
let abs_path = worktree.absolutize(path);
|
||||||
settings_contents.push(async move {
|
settings_contents.push(async move {
|
||||||
(settings_dir, (!removed).then_some(fs.load(&abs_path).await))
|
(
|
||||||
|
settings_dir,
|
||||||
|
if removed {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(async move { fs.load(&abs_path?).await }.await)
|
||||||
|
},
|
||||||
|
)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8718,6 +8734,20 @@ fn relativize_path(base: &Path, path: &Path) -> PathBuf {
|
||||||
components.iter().map(|c| c.as_os_str()).collect()
|
components.iter().map(|c| c.as_os_str()).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn resolve_path(base: &Path, path: &Path) -> PathBuf {
|
||||||
|
let mut result = base.to_path_buf();
|
||||||
|
for component in path.components() {
|
||||||
|
match component {
|
||||||
|
Component::ParentDir => {
|
||||||
|
result.pop();
|
||||||
|
}
|
||||||
|
Component::CurDir => (),
|
||||||
|
_ => result.push(component),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
impl Item for Buffer {
|
impl Item for Buffer {
|
||||||
fn entry_id(&self, cx: &AppContext) -> Option<ProjectEntryId> {
|
fn entry_id(&self, cx: &AppContext) -> Option<ProjectEntryId> {
|
||||||
File::from_dyn(self.file()).and_then(|file| file.project_entry_id(cx))
|
File::from_dyn(self.file()).and_then(|file| file.project_entry_id(cx))
|
||||||
|
|
|
@ -4278,6 +4278,75 @@ fn test_glob_literal_prefix() {
|
||||||
assert_eq!(glob_literal_prefix("foo/bar/baz.js"), "foo/bar/baz.js");
|
assert_eq!(glob_literal_prefix("foo/bar/baz.js"), "foo/bar/baz.js");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_create_entry(cx: &mut gpui::TestAppContext) {
|
||||||
|
init_test(cx);
|
||||||
|
|
||||||
|
let fs = FakeFs::new(cx.executor().clone());
|
||||||
|
fs.insert_tree(
|
||||||
|
"/one/two",
|
||||||
|
json!({
|
||||||
|
"three": {
|
||||||
|
"a.txt": "",
|
||||||
|
"four": {}
|
||||||
|
},
|
||||||
|
"c.rs": ""
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let project = Project::test(fs.clone(), ["/one/two/three".as_ref()], cx).await;
|
||||||
|
project
|
||||||
|
.update(cx, |project, cx| {
|
||||||
|
let id = project.worktrees().next().unwrap().read(cx).id();
|
||||||
|
project.create_entry((id, "b.."), true, cx)
|
||||||
|
})
|
||||||
|
.unwrap()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Can't create paths outside the project
|
||||||
|
let result = project
|
||||||
|
.update(cx, |project, cx| {
|
||||||
|
let id = project.worktrees().next().unwrap().read(cx).id();
|
||||||
|
project.create_entry((id, "../../boop"), true, cx)
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
assert!(result.is_err());
|
||||||
|
|
||||||
|
// Can't create paths with '..'
|
||||||
|
let result = project
|
||||||
|
.update(cx, |project, cx| {
|
||||||
|
let id = project.worktrees().next().unwrap().read(cx).id();
|
||||||
|
project.create_entry((id, "four/../beep"), true, cx)
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
assert!(result.is_err());
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
fs.paths(true),
|
||||||
|
vec![
|
||||||
|
PathBuf::from("/"),
|
||||||
|
PathBuf::from("/one"),
|
||||||
|
PathBuf::from("/one/two"),
|
||||||
|
PathBuf::from("/one/two/c.rs"),
|
||||||
|
PathBuf::from("/one/two/three"),
|
||||||
|
PathBuf::from("/one/two/three/a.txt"),
|
||||||
|
PathBuf::from("/one/two/three/b.."),
|
||||||
|
PathBuf::from("/one/two/three/four"),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
// And we cannot open buffers with '..'
|
||||||
|
let result = project
|
||||||
|
.update(cx, |project, cx| {
|
||||||
|
let id = project.worktrees().next().unwrap().read(cx).id();
|
||||||
|
project.open_buffer((id, "../c.rs"), cx)
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
assert!(result.is_err())
|
||||||
|
}
|
||||||
|
|
||||||
async fn search(
|
async fn search(
|
||||||
project: &Model<Project>,
|
project: &Model<Project>,
|
||||||
query: SearchQuery,
|
query: SearchQuery,
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue