Merge branch 'main' into editor2_tests
This commit is contained in:
commit
4cb4033a36
70 changed files with 3367 additions and 6799 deletions
59
Cargo.lock
generated
59
Cargo.lock
generated
|
@ -1095,6 +1095,23 @@ dependencies = [
|
||||||
"workspace",
|
"workspace",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "breadcrumbs2"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"collections",
|
||||||
|
"editor2",
|
||||||
|
"gpui2",
|
||||||
|
"itertools 0.10.5",
|
||||||
|
"language2",
|
||||||
|
"project2",
|
||||||
|
"search2",
|
||||||
|
"settings2",
|
||||||
|
"theme2",
|
||||||
|
"ui2",
|
||||||
|
"workspace2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bromberg_sl2"
|
name = "bromberg_sl2"
|
||||||
version = "0.6.0"
|
version = "0.6.0"
|
||||||
|
@ -1688,7 +1705,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "collab"
|
name = "collab"
|
||||||
version = "0.29.0"
|
version = "0.29.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
|
@ -2126,6 +2143,25 @@ dependencies = [
|
||||||
"workspace",
|
"workspace",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "copilot_button2"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"copilot2",
|
||||||
|
"editor2",
|
||||||
|
"fs2",
|
||||||
|
"futures 0.3.28",
|
||||||
|
"gpui2",
|
||||||
|
"language2",
|
||||||
|
"settings2",
|
||||||
|
"smol",
|
||||||
|
"theme2",
|
||||||
|
"util",
|
||||||
|
"workspace2",
|
||||||
|
"zed_actions2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "core-foundation"
|
name = "core-foundation"
|
||||||
version = "0.9.3"
|
version = "0.9.3"
|
||||||
|
@ -4774,6 +4810,24 @@ dependencies = [
|
||||||
"workspace",
|
"workspace",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "language_selector2"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"editor2",
|
||||||
|
"fuzzy2",
|
||||||
|
"gpui2",
|
||||||
|
"language2",
|
||||||
|
"picker2",
|
||||||
|
"project2",
|
||||||
|
"settings2",
|
||||||
|
"theme2",
|
||||||
|
"ui2",
|
||||||
|
"util",
|
||||||
|
"workspace2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "language_tools"
|
name = "language_tools"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
@ -11726,6 +11780,7 @@ dependencies = [
|
||||||
"audio2",
|
"audio2",
|
||||||
"auto_update2",
|
"auto_update2",
|
||||||
"backtrace",
|
"backtrace",
|
||||||
|
"breadcrumbs2",
|
||||||
"call2",
|
"call2",
|
||||||
"channel2",
|
"channel2",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
@ -11735,6 +11790,7 @@ dependencies = [
|
||||||
"collections",
|
"collections",
|
||||||
"command_palette2",
|
"command_palette2",
|
||||||
"copilot2",
|
"copilot2",
|
||||||
|
"copilot_button2",
|
||||||
"ctor",
|
"ctor",
|
||||||
"db2",
|
"db2",
|
||||||
"diagnostics2",
|
"diagnostics2",
|
||||||
|
@ -11754,6 +11810,7 @@ dependencies = [
|
||||||
"isahc",
|
"isahc",
|
||||||
"journal2",
|
"journal2",
|
||||||
"language2",
|
"language2",
|
||||||
|
"language_selector2",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
|
|
|
@ -9,6 +9,7 @@ members = [
|
||||||
"crates/auto_update",
|
"crates/auto_update",
|
||||||
"crates/auto_update2",
|
"crates/auto_update2",
|
||||||
"crates/breadcrumbs",
|
"crates/breadcrumbs",
|
||||||
|
"crates/breadcrumbs2",
|
||||||
"crates/call",
|
"crates/call",
|
||||||
"crates/call2",
|
"crates/call2",
|
||||||
"crates/channel",
|
"crates/channel",
|
||||||
|
@ -60,6 +61,7 @@ members = [
|
||||||
"crates/language",
|
"crates/language",
|
||||||
"crates/language2",
|
"crates/language2",
|
||||||
"crates/language_selector",
|
"crates/language_selector",
|
||||||
|
"crates/language_selector2",
|
||||||
"crates/language_tools",
|
"crates/language_tools",
|
||||||
"crates/live_kit_client",
|
"crates/live_kit_client",
|
||||||
"crates/live_kit_server",
|
"crates/live_kit_server",
|
||||||
|
|
1
assets/icons/copy.svg
Normal file
1
assets/icons/copy.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-copy"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>
|
After Width: | Height: | Size: 338 B |
|
@ -102,7 +102,7 @@ pub fn init(http_client: Arc<dyn HttpClient>, server_url: String, cx: &mut AppCo
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
if let Some(version) = *ZED_APP_VERSION {
|
if let Some(version) = ZED_APP_VERSION.or_else(|| cx.app_metadata().app_version) {
|
||||||
let auto_updater = cx.build_model(|cx| {
|
let auto_updater = cx.build_model(|cx| {
|
||||||
let updater = AutoUpdater::new(version, http_client, server_url);
|
let updater = AutoUpdater::new(version, http_client, server_url);
|
||||||
|
|
||||||
|
|
28
crates/breadcrumbs2/Cargo.toml
Normal file
28
crates/breadcrumbs2/Cargo.toml
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
[package]
|
||||||
|
name = "breadcrumbs2"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "src/breadcrumbs.rs"
|
||||||
|
doctest = false
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
collections = { path = "../collections" }
|
||||||
|
editor = { package = "editor2", path = "../editor2" }
|
||||||
|
gpui = { package = "gpui2", path = "../gpui2" }
|
||||||
|
ui = { package = "ui2", path = "../ui2" }
|
||||||
|
language = { package = "language2", path = "../language2" }
|
||||||
|
project = { package = "project2", path = "../project2" }
|
||||||
|
search = { package = "search2", path = "../search2" }
|
||||||
|
settings = { package = "settings2", path = "../settings2" }
|
||||||
|
theme = { package = "theme2", path = "../theme2" }
|
||||||
|
workspace = { package = "workspace2", path = "../workspace2" }
|
||||||
|
# outline = { path = "../outline" }
|
||||||
|
itertools = "0.10"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
|
||||||
|
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
|
||||||
|
workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] }
|
204
crates/breadcrumbs2/src/breadcrumbs.rs
Normal file
204
crates/breadcrumbs2/src/breadcrumbs.rs
Normal file
|
@ -0,0 +1,204 @@
|
||||||
|
use gpui::{
|
||||||
|
Component, Element, EventEmitter, IntoElement, ParentElement, Render, StyledText, Subscription,
|
||||||
|
ViewContext, WeakView,
|
||||||
|
};
|
||||||
|
use itertools::Itertools;
|
||||||
|
use theme::ActiveTheme;
|
||||||
|
use ui::{ButtonCommon, ButtonLike, ButtonStyle, Clickable, Disableable, Label};
|
||||||
|
use workspace::{
|
||||||
|
item::{ItemEvent, ItemHandle},
|
||||||
|
ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub enum Event {
|
||||||
|
UpdateLocation,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Breadcrumbs {
|
||||||
|
pane_focused: bool,
|
||||||
|
active_item: Option<Box<dyn ItemHandle>>,
|
||||||
|
subscription: Option<Subscription>,
|
||||||
|
_workspace: WeakView<Workspace>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Breadcrumbs {
|
||||||
|
pub fn new(workspace: &Workspace) -> Self {
|
||||||
|
Self {
|
||||||
|
pane_focused: false,
|
||||||
|
active_item: Default::default(),
|
||||||
|
subscription: Default::default(),
|
||||||
|
_workspace: workspace.weak_handle(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventEmitter<Event> for Breadcrumbs {}
|
||||||
|
impl EventEmitter<ToolbarItemEvent> for Breadcrumbs {}
|
||||||
|
|
||||||
|
impl Render for Breadcrumbs {
|
||||||
|
type Element = Component<ButtonLike>;
|
||||||
|
|
||||||
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||||
|
let button = ButtonLike::new("breadcrumbs")
|
||||||
|
.style(ButtonStyle::Transparent)
|
||||||
|
.disabled(true);
|
||||||
|
|
||||||
|
let active_item = match &self.active_item {
|
||||||
|
Some(active_item) => active_item,
|
||||||
|
None => return button.into_element(),
|
||||||
|
};
|
||||||
|
let not_editor = active_item.downcast::<editor::Editor>().is_none();
|
||||||
|
|
||||||
|
let breadcrumbs = match active_item.breadcrumbs(cx.theme(), cx) {
|
||||||
|
Some(breadcrumbs) => breadcrumbs,
|
||||||
|
None => return button.into_element(),
|
||||||
|
}
|
||||||
|
.into_iter()
|
||||||
|
.map(|breadcrumb| {
|
||||||
|
StyledText::new(breadcrumb.text)
|
||||||
|
.with_highlights(&cx.text_style(), breadcrumb.highlights.unwrap_or_default())
|
||||||
|
.into_any()
|
||||||
|
});
|
||||||
|
|
||||||
|
let button = button.children(Itertools::intersperse_with(breadcrumbs, || {
|
||||||
|
Label::new(" › ").into_any_element()
|
||||||
|
}));
|
||||||
|
|
||||||
|
if not_editor || !self.pane_focused {
|
||||||
|
return button.into_element();
|
||||||
|
}
|
||||||
|
|
||||||
|
// let this = cx.view().downgrade();
|
||||||
|
button
|
||||||
|
.style(ButtonStyle::Filled)
|
||||||
|
.disabled(false)
|
||||||
|
.on_click(move |_, _cx| {
|
||||||
|
todo!("outline::toggle");
|
||||||
|
// this.update(cx, |this, cx| {
|
||||||
|
// if let Some(workspace) = this.workspace.upgrade() {
|
||||||
|
// workspace.update(cx, |_workspace, _cx| {
|
||||||
|
// outline::toggle(workspace, &Default::default(), cx)
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// .ok();
|
||||||
|
})
|
||||||
|
.into_element()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// impl View for Breadcrumbs {
|
||||||
|
// fn ui_name() -> &'static str {
|
||||||
|
// "Breadcrumbs"
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||||
|
// let active_item = match &self.active_item {
|
||||||
|
// Some(active_item) => active_item,
|
||||||
|
// None => return Empty::new().into_any(),
|
||||||
|
// };
|
||||||
|
// let not_editor = active_item.downcast::<editor::Editor>().is_none();
|
||||||
|
|
||||||
|
// let theme = theme::current(cx).clone();
|
||||||
|
// let style = &theme.workspace.toolbar.breadcrumbs;
|
||||||
|
|
||||||
|
// let breadcrumbs = match active_item.breadcrumbs(&theme, cx) {
|
||||||
|
// Some(breadcrumbs) => breadcrumbs,
|
||||||
|
// None => return Empty::new().into_any(),
|
||||||
|
// }
|
||||||
|
// .into_iter()
|
||||||
|
// .map(|breadcrumb| {
|
||||||
|
// Text::new(
|
||||||
|
// breadcrumb.text,
|
||||||
|
// theme.workspace.toolbar.breadcrumbs.default.text.clone(),
|
||||||
|
// )
|
||||||
|
// .with_highlights(breadcrumb.highlights.unwrap_or_default())
|
||||||
|
// .into_any()
|
||||||
|
// });
|
||||||
|
|
||||||
|
// let crumbs = Flex::row()
|
||||||
|
// .with_children(Itertools::intersperse_with(breadcrumbs, || {
|
||||||
|
// Label::new(" › ", style.default.text.clone()).into_any()
|
||||||
|
// }))
|
||||||
|
// .constrained()
|
||||||
|
// .with_height(theme.workspace.toolbar.breadcrumb_height)
|
||||||
|
// .contained();
|
||||||
|
|
||||||
|
// if not_editor || !self.pane_focused {
|
||||||
|
// return crumbs
|
||||||
|
// .with_style(style.default.container)
|
||||||
|
// .aligned()
|
||||||
|
// .left()
|
||||||
|
// .into_any();
|
||||||
|
// }
|
||||||
|
|
||||||
|
// MouseEventHandler::new::<Breadcrumbs, _>(0, cx, |state, _| {
|
||||||
|
// let style = style.style_for(state);
|
||||||
|
// crumbs.with_style(style.container)
|
||||||
|
// })
|
||||||
|
// .on_click(MouseButton::Left, |_, this, cx| {
|
||||||
|
// if let Some(workspace) = this.workspace.upgrade(cx) {
|
||||||
|
// workspace.update(cx, |workspace, cx| {
|
||||||
|
// outline::toggle(workspace, &Default::default(), cx)
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// .with_tooltip::<Breadcrumbs>(
|
||||||
|
// 0,
|
||||||
|
// "Show symbol outline".to_owned(),
|
||||||
|
// Some(Box::new(outline::Toggle)),
|
||||||
|
// theme.tooltip.clone(),
|
||||||
|
// cx,
|
||||||
|
// )
|
||||||
|
// .aligned()
|
||||||
|
// .left()
|
||||||
|
// .into_any()
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
impl ToolbarItemView for Breadcrumbs {
|
||||||
|
fn set_active_pane_item(
|
||||||
|
&mut self,
|
||||||
|
active_pane_item: Option<&dyn ItemHandle>,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) -> ToolbarItemLocation {
|
||||||
|
cx.notify();
|
||||||
|
self.active_item = None;
|
||||||
|
if let Some(item) = active_pane_item {
|
||||||
|
let this = cx.view().downgrade();
|
||||||
|
self.subscription = Some(item.subscribe_to_item_events(
|
||||||
|
cx,
|
||||||
|
Box::new(move |event, cx| {
|
||||||
|
if let ItemEvent::UpdateBreadcrumbs = event {
|
||||||
|
this.update(cx, |_, cx| {
|
||||||
|
cx.emit(Event::UpdateLocation);
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
));
|
||||||
|
self.active_item = Some(item.boxed_clone());
|
||||||
|
item.breadcrumb_location(cx)
|
||||||
|
} else {
|
||||||
|
ToolbarItemLocation::Hidden
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fn location_for_event(
|
||||||
|
// &self,
|
||||||
|
// _: &Event,
|
||||||
|
// current_location: ToolbarItemLocation,
|
||||||
|
// cx: &AppContext,
|
||||||
|
// ) -> ToolbarItemLocation {
|
||||||
|
// if let Some(active_item) = self.active_item.as_ref() {
|
||||||
|
// active_item.breadcrumb_location(cx)
|
||||||
|
// } else {
|
||||||
|
// current_location
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
fn pane_focus_update(&mut self, pane_focused: bool, _: &mut ViewContext<Self>) {
|
||||||
|
self.pane_focused = pane_focused;
|
||||||
|
}
|
||||||
|
}
|
|
@ -346,7 +346,7 @@ impl<T: Entity> Drop for PendingEntitySubscription<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone)]
|
#[derive(Debug, Copy, Clone)]
|
||||||
pub struct TelemetrySettings {
|
pub struct TelemetrySettings {
|
||||||
pub diagnostics: bool,
|
pub diagnostics: bool,
|
||||||
pub metrics: bool,
|
pub metrics: bool,
|
||||||
|
|
|
@ -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.29.0"
|
version = "0.29.1"
|
||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
|
|
|
@ -1220,6 +1220,13 @@ impl Database {
|
||||||
self.check_user_is_channel_admin(&new_parent, admin_id, &*tx)
|
self.check_user_is_channel_admin(&new_parent, admin_id, &*tx)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
if new_parent
|
||||||
|
.ancestors_including_self()
|
||||||
|
.any(|id| id == channel.id)
|
||||||
|
{
|
||||||
|
Err(anyhow!("cannot move a channel into one of its descendants"))?;
|
||||||
|
}
|
||||||
|
|
||||||
new_parent_path = new_parent.path();
|
new_parent_path = new_parent.path();
|
||||||
new_parent_channel = Some(new_parent);
|
new_parent_channel = Some(new_parent);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -450,6 +450,20 @@ async fn test_db_channel_moving_bugs(db: &Arc<Database>) {
|
||||||
(livestreaming_id, &[projects_id]),
|
(livestreaming_id, &[projects_id]),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Can't move a channel into its ancestor
|
||||||
|
db.move_channel(projects_id, Some(livestreaming_id), user_id)
|
||||||
|
.await
|
||||||
|
.unwrap_err();
|
||||||
|
let result = db.get_channels_for_user(user_id).await.unwrap();
|
||||||
|
assert_channel_tree(
|
||||||
|
result.channels,
|
||||||
|
&[
|
||||||
|
(zed_id, &[]),
|
||||||
|
(projects_id, &[]),
|
||||||
|
(livestreaming_id, &[projects_id]),
|
||||||
|
],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
test_both_dbs!(
|
test_both_dbs!(
|
||||||
|
|
|
@ -1220,6 +1220,13 @@ impl Database {
|
||||||
self.check_user_is_channel_admin(&new_parent, admin_id, &*tx)
|
self.check_user_is_channel_admin(&new_parent, admin_id, &*tx)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
if new_parent
|
||||||
|
.ancestors_including_self()
|
||||||
|
.any(|id| id == channel.id)
|
||||||
|
{
|
||||||
|
Err(anyhow!("cannot move a channel into one of its descendants"))?;
|
||||||
|
}
|
||||||
|
|
||||||
new_parent_path = new_parent.path();
|
new_parent_path = new_parent.path();
|
||||||
new_parent_channel = Some(new_parent);
|
new_parent_channel = Some(new_parent);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -420,8 +420,6 @@ async fn test_db_channel_moving_bugs(db: &Arc<Database>) {
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// Dag is: zed - projects - livestreaming
|
|
||||||
|
|
||||||
// Move to same parent should be a no-op
|
// Move to same parent should be a no-op
|
||||||
assert!(db
|
assert!(db
|
||||||
.move_channel(projects_id, Some(zed_id), user_id)
|
.move_channel(projects_id, Some(zed_id), user_id)
|
||||||
|
@ -450,6 +448,20 @@ async fn test_db_channel_moving_bugs(db: &Arc<Database>) {
|
||||||
(livestreaming_id, &[projects_id]),
|
(livestreaming_id, &[projects_id]),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Can't move a channel into its ancestor
|
||||||
|
db.move_channel(projects_id, Some(livestreaming_id), user_id)
|
||||||
|
.await
|
||||||
|
.unwrap_err();
|
||||||
|
let result = db.get_channels_for_user(user_id).await.unwrap();
|
||||||
|
assert_channel_tree(
|
||||||
|
result.channels,
|
||||||
|
&[
|
||||||
|
(zed_id, &[]),
|
||||||
|
(projects_id, &[]),
|
||||||
|
(livestreaming_id, &[projects_id]),
|
||||||
|
],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
test_both_dbs!(
|
test_both_dbs!(
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
#![allow(unused)]
|
#![allow(unused)]
|
||||||
// mod channel_modal;
|
mod channel_modal;
|
||||||
mod contact_finder;
|
mod contact_finder;
|
||||||
|
|
||||||
// use crate::{
|
// use crate::{
|
||||||
|
@ -192,6 +192,8 @@ use workspace::{
|
||||||
|
|
||||||
use crate::{face_pile::FacePile, CollaborationPanelSettings};
|
use crate::{face_pile::FacePile, CollaborationPanelSettings};
|
||||||
|
|
||||||
|
use self::channel_modal::ChannelModal;
|
||||||
|
|
||||||
pub fn init(cx: &mut AppContext) {
|
pub fn init(cx: &mut AppContext) {
|
||||||
cx.observe_new_views(|workspace: &mut Workspace, _| {
|
cx.observe_new_views(|workspace: &mut Workspace, _| {
|
||||||
workspace.register_action(|workspace, _: &ToggleFocus, cx| {
|
workspace.register_action(|workspace, _: &ToggleFocus, cx| {
|
||||||
|
@ -2058,13 +2060,11 @@ impl CollabPanel {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn invite_members(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
|
fn invite_members(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
|
||||||
todo!();
|
self.show_channel_modal(channel_id, channel_modal::Mode::InviteMembers, cx);
|
||||||
// self.show_channel_modal(channel_id, channel_modal::Mode::InviteMembers, cx);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn manage_members(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
|
fn manage_members(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
|
||||||
todo!();
|
self.show_channel_modal(channel_id, channel_modal::Mode::ManageMembers, cx);
|
||||||
// self.show_channel_modal(channel_id, channel_modal::Mode::ManageMembers, cx);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remove_selected_channel(&mut self, _: &Remove, cx: &mut ViewContext<Self>) {
|
fn remove_selected_channel(&mut self, _: &Remove, cx: &mut ViewContext<Self>) {
|
||||||
|
@ -2156,38 +2156,36 @@ impl CollabPanel {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// fn show_channel_modal(
|
fn show_channel_modal(
|
||||||
// &mut self,
|
&mut self,
|
||||||
// channel_id: ChannelId,
|
channel_id: ChannelId,
|
||||||
// mode: channel_modal::Mode,
|
mode: channel_modal::Mode,
|
||||||
// cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
// ) {
|
) {
|
||||||
// let workspace = self.workspace.clone();
|
let workspace = self.workspace.clone();
|
||||||
// let user_store = self.user_store.clone();
|
let user_store = self.user_store.clone();
|
||||||
// let channel_store = self.channel_store.clone();
|
let channel_store = self.channel_store.clone();
|
||||||
// let members = self.channel_store.update(cx, |channel_store, cx| {
|
let members = self.channel_store.update(cx, |channel_store, cx| {
|
||||||
// channel_store.get_channel_member_details(channel_id, cx)
|
channel_store.get_channel_member_details(channel_id, cx)
|
||||||
// });
|
});
|
||||||
|
|
||||||
// cx.spawn(|_, mut cx| async move {
|
cx.spawn(|_, mut cx| async move {
|
||||||
// let members = members.await?;
|
let members = members.await?;
|
||||||
// workspace.update(&mut cx, |workspace, cx| {
|
workspace.update(&mut cx, |workspace, cx| {
|
||||||
// workspace.toggle_modal(cx, |_, cx| {
|
workspace.toggle_modal(cx, |cx| {
|
||||||
// cx.add_view(|cx| {
|
ChannelModal::new(
|
||||||
// ChannelModal::new(
|
user_store.clone(),
|
||||||
// user_store.clone(),
|
channel_store.clone(),
|
||||||
// channel_store.clone(),
|
channel_id,
|
||||||
// channel_id,
|
mode,
|
||||||
// mode,
|
members,
|
||||||
// members,
|
cx,
|
||||||
// cx,
|
)
|
||||||
// )
|
});
|
||||||
// })
|
})
|
||||||
// });
|
})
|
||||||
// })
|
.detach();
|
||||||
// })
|
}
|
||||||
// .detach();
|
|
||||||
// }
|
|
||||||
|
|
||||||
// fn remove_selected_channel(&mut self, action: &RemoveChannel, cx: &mut ViewContext<Self>) {
|
// fn remove_selected_channel(&mut self, action: &RemoveChannel, cx: &mut ViewContext<Self>) {
|
||||||
// self.remove_channel(action.channel_id, cx)
|
// self.remove_channel(action.channel_id, cx)
|
||||||
|
|
|
@ -3,58 +3,54 @@ use client::{
|
||||||
proto::{self, ChannelRole, ChannelVisibility},
|
proto::{self, ChannelRole, ChannelVisibility},
|
||||||
User, UserId, UserStore,
|
User, UserId, UserStore,
|
||||||
};
|
};
|
||||||
use context_menu::{ContextMenu, ContextMenuItem};
|
|
||||||
use fuzzy::{match_strings, StringMatchCandidate};
|
use fuzzy::{match_strings, StringMatchCandidate};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions,
|
actions, div, AppContext, ClipboardItem, DismissEvent, Div, Entity, EventEmitter,
|
||||||
elements::*,
|
FocusableView, Model, ParentElement, Render, Styled, Task, View, ViewContext, VisualContext,
|
||||||
platform::{CursorStyle, MouseButton},
|
WeakView,
|
||||||
AppContext, ClipboardItem, Entity, ModelHandle, MouseState, Task, View, ViewContext,
|
|
||||||
ViewHandle,
|
|
||||||
};
|
};
|
||||||
use picker::{Picker, PickerDelegate, PickerEvent};
|
use picker::{Picker, PickerDelegate};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use ui::v_stack;
|
||||||
use util::TryFutureExt;
|
use util::TryFutureExt;
|
||||||
use workspace::Modal;
|
|
||||||
|
|
||||||
actions!(
|
actions!(
|
||||||
channel_modal,
|
|
||||||
[
|
|
||||||
SelectNextControl,
|
SelectNextControl,
|
||||||
ToggleMode,
|
ToggleMode,
|
||||||
ToggleMemberAdmin,
|
ToggleMemberAdmin,
|
||||||
RemoveMember
|
RemoveMember
|
||||||
]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
pub fn init(cx: &mut AppContext) {
|
// pub fn init(cx: &mut AppContext) {
|
||||||
Picker::<ChannelModalDelegate>::init(cx);
|
// Picker::<ChannelModalDelegate>::init(cx);
|
||||||
cx.add_action(ChannelModal::toggle_mode);
|
// cx.add_action(ChannelModal::toggle_mode);
|
||||||
cx.add_action(ChannelModal::toggle_member_admin);
|
// cx.add_action(ChannelModal::toggle_member_admin);
|
||||||
cx.add_action(ChannelModal::remove_member);
|
// cx.add_action(ChannelModal::remove_member);
|
||||||
cx.add_action(ChannelModal::dismiss);
|
// cx.add_action(ChannelModal::dismiss);
|
||||||
}
|
// }
|
||||||
|
|
||||||
pub struct ChannelModal {
|
pub struct ChannelModal {
|
||||||
picker: ViewHandle<Picker<ChannelModalDelegate>>,
|
picker: View<Picker<ChannelModalDelegate>>,
|
||||||
channel_store: ModelHandle<ChannelStore>,
|
channel_store: Model<ChannelStore>,
|
||||||
channel_id: ChannelId,
|
channel_id: ChannelId,
|
||||||
has_focus: bool,
|
has_focus: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ChannelModal {
|
impl ChannelModal {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
user_store: ModelHandle<UserStore>,
|
user_store: Model<UserStore>,
|
||||||
channel_store: ModelHandle<ChannelStore>,
|
channel_store: Model<ChannelStore>,
|
||||||
channel_id: ChannelId,
|
channel_id: ChannelId,
|
||||||
mode: Mode,
|
mode: Mode,
|
||||||
members: Vec<ChannelMembership>,
|
members: Vec<ChannelMembership>,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
cx.observe(&channel_store, |_, _, cx| cx.notify()).detach();
|
cx.observe(&channel_store, |_, _, cx| cx.notify()).detach();
|
||||||
let picker = cx.add_view(|cx| {
|
let channel_modal = cx.view().downgrade();
|
||||||
|
let picker = cx.build_view(|cx| {
|
||||||
Picker::new(
|
Picker::new(
|
||||||
ChannelModalDelegate {
|
ChannelModalDelegate {
|
||||||
|
channel_modal,
|
||||||
matching_users: Vec::new(),
|
matching_users: Vec::new(),
|
||||||
matching_member_indices: Vec::new(),
|
matching_member_indices: Vec::new(),
|
||||||
selected_index: 0,
|
selected_index: 0,
|
||||||
|
@ -64,20 +60,17 @@ impl ChannelModal {
|
||||||
match_candidates: Vec::new(),
|
match_candidates: Vec::new(),
|
||||||
members,
|
members,
|
||||||
mode,
|
mode,
|
||||||
context_menu: cx.add_view(|cx| {
|
// context_menu: cx.add_view(|cx| {
|
||||||
let mut menu = ContextMenu::new(cx.view_id(), cx);
|
// let mut menu = ContextMenu::new(cx.view_id(), cx);
|
||||||
menu.set_position_mode(OverlayPositionMode::Local);
|
// menu.set_position_mode(OverlayPositionMode::Local);
|
||||||
menu
|
// menu
|
||||||
}),
|
// }),
|
||||||
},
|
},
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
.with_theme(|theme| theme.collab_panel.tabbed_modal.picker.clone())
|
|
||||||
});
|
});
|
||||||
|
|
||||||
cx.subscribe(&picker, |_, _, e, cx| cx.emit(*e)).detach();
|
let has_focus = picker.focus_handle(cx).contains_focused(cx);
|
||||||
|
|
||||||
let has_focus = picker.read(cx).has_focus();
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
picker,
|
picker,
|
||||||
|
@ -88,7 +81,7 @@ impl ChannelModal {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn toggle_mode(&mut self, _: &ToggleMode, cx: &mut ViewContext<Self>) {
|
fn toggle_mode(&mut self, _: &ToggleMode, cx: &mut ViewContext<Self>) {
|
||||||
let mode = match self.picker.read(cx).delegate().mode {
|
let mode = match self.picker.read(cx).delegate.mode {
|
||||||
Mode::ManageMembers => Mode::InviteMembers,
|
Mode::ManageMembers => Mode::InviteMembers,
|
||||||
Mode::InviteMembers => Mode::ManageMembers,
|
Mode::InviteMembers => Mode::ManageMembers,
|
||||||
};
|
};
|
||||||
|
@ -103,20 +96,20 @@ impl ChannelModal {
|
||||||
let mut members = channel_store
|
let mut members = channel_store
|
||||||
.update(&mut cx, |channel_store, cx| {
|
.update(&mut cx, |channel_store, cx| {
|
||||||
channel_store.get_channel_member_details(channel_id, cx)
|
channel_store.get_channel_member_details(channel_id, cx)
|
||||||
})
|
})?
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
members.sort_by(|a, b| a.sort_key().cmp(&b.sort_key()));
|
members.sort_by(|a, b| a.sort_key().cmp(&b.sort_key()));
|
||||||
|
|
||||||
this.update(&mut cx, |this, cx| {
|
this.update(&mut cx, |this, cx| {
|
||||||
this.picker
|
this.picker
|
||||||
.update(cx, |picker, _| picker.delegate_mut().members = members);
|
.update(cx, |picker, _| picker.delegate.members = members);
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.update(&mut cx, |this, cx| {
|
this.update(&mut cx, |this, cx| {
|
||||||
this.picker.update(cx, |picker, cx| {
|
this.picker.update(cx, |picker, cx| {
|
||||||
let delegate = picker.delegate_mut();
|
let delegate = &mut picker.delegate;
|
||||||
delegate.mode = mode;
|
delegate.mode = mode;
|
||||||
delegate.selected_index = 0;
|
delegate.selected_index = 0;
|
||||||
picker.set_query("", cx);
|
picker.set_query("", cx);
|
||||||
|
@ -131,203 +124,194 @@ impl ChannelModal {
|
||||||
|
|
||||||
fn toggle_member_admin(&mut self, _: &ToggleMemberAdmin, cx: &mut ViewContext<Self>) {
|
fn toggle_member_admin(&mut self, _: &ToggleMemberAdmin, cx: &mut ViewContext<Self>) {
|
||||||
self.picker.update(cx, |picker, cx| {
|
self.picker.update(cx, |picker, cx| {
|
||||||
picker.delegate_mut().toggle_selected_member_admin(cx);
|
picker.delegate.toggle_selected_member_admin(cx);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remove_member(&mut self, _: &RemoveMember, cx: &mut ViewContext<Self>) {
|
fn remove_member(&mut self, _: &RemoveMember, cx: &mut ViewContext<Self>) {
|
||||||
self.picker.update(cx, |picker, cx| {
|
self.picker.update(cx, |picker, cx| {
|
||||||
picker.delegate_mut().remove_selected_member(cx);
|
picker.delegate.remove_selected_member(cx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dismiss(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
|
fn dismiss(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
|
||||||
cx.emit(PickerEvent::Dismiss);
|
cx.emit(DismissEvent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Entity for ChannelModal {
|
impl EventEmitter<DismissEvent> for ChannelModal {}
|
||||||
type Event = PickerEvent;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl View for ChannelModal {
|
impl FocusableView for ChannelModal {
|
||||||
fn ui_name() -> &'static str {
|
fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
|
||||||
"ChannelModal"
|
self.picker.focus_handle(cx)
|
||||||
}
|
|
||||||
|
|
||||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
|
||||||
let theme = &theme::current(cx).collab_panel.tabbed_modal;
|
|
||||||
|
|
||||||
let mode = self.picker.read(cx).delegate().mode;
|
|
||||||
let Some(channel) = self.channel_store.read(cx).channel_for_id(self.channel_id) else {
|
|
||||||
return Empty::new().into_any();
|
|
||||||
};
|
|
||||||
|
|
||||||
enum InviteMembers {}
|
|
||||||
enum ManageMembers {}
|
|
||||||
|
|
||||||
fn render_mode_button<T: 'static>(
|
|
||||||
mode: Mode,
|
|
||||||
text: &'static str,
|
|
||||||
current_mode: Mode,
|
|
||||||
theme: &theme::TabbedModal,
|
|
||||||
cx: &mut ViewContext<ChannelModal>,
|
|
||||||
) -> AnyElement<ChannelModal> {
|
|
||||||
let active = mode == current_mode;
|
|
||||||
MouseEventHandler::new::<T, _>(0, cx, move |state, _| {
|
|
||||||
let contained_text = theme.tab_button.style_for(active, state);
|
|
||||||
Label::new(text, contained_text.text.clone())
|
|
||||||
.contained()
|
|
||||||
.with_style(contained_text.container.clone())
|
|
||||||
})
|
|
||||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
|
||||||
if !active {
|
|
||||||
this.set_mode(mode, cx);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.with_cursor_style(CursorStyle::PointingHand)
|
|
||||||
.into_any()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_visibility(
|
|
||||||
channel_id: ChannelId,
|
|
||||||
visibility: ChannelVisibility,
|
|
||||||
theme: &theme::TabbedModal,
|
|
||||||
cx: &mut ViewContext<ChannelModal>,
|
|
||||||
) -> AnyElement<ChannelModal> {
|
|
||||||
enum TogglePublic {}
|
|
||||||
|
|
||||||
if visibility == ChannelVisibility::Members {
|
|
||||||
return Flex::row()
|
|
||||||
.with_child(
|
|
||||||
MouseEventHandler::new::<TogglePublic, _>(0, cx, move |state, _| {
|
|
||||||
let style = theme.visibility_toggle.style_for(state);
|
|
||||||
Label::new(format!("{}", "Public access: OFF"), style.text.clone())
|
|
||||||
.contained()
|
|
||||||
.with_style(style.container.clone())
|
|
||||||
})
|
|
||||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
|
||||||
this.channel_store
|
|
||||||
.update(cx, |channel_store, cx| {
|
|
||||||
channel_store.set_channel_visibility(
|
|
||||||
channel_id,
|
|
||||||
ChannelVisibility::Public,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.detach_and_log_err(cx);
|
|
||||||
})
|
|
||||||
.with_cursor_style(CursorStyle::PointingHand),
|
|
||||||
)
|
|
||||||
.into_any();
|
|
||||||
}
|
|
||||||
|
|
||||||
Flex::row()
|
|
||||||
.with_child(
|
|
||||||
MouseEventHandler::new::<TogglePublic, _>(0, cx, move |state, _| {
|
|
||||||
let style = theme.visibility_toggle.style_for(state);
|
|
||||||
Label::new(format!("{}", "Public access: ON"), style.text.clone())
|
|
||||||
.contained()
|
|
||||||
.with_style(style.container.clone())
|
|
||||||
})
|
|
||||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
|
||||||
this.channel_store
|
|
||||||
.update(cx, |channel_store, cx| {
|
|
||||||
channel_store.set_channel_visibility(
|
|
||||||
channel_id,
|
|
||||||
ChannelVisibility::Members,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.detach_and_log_err(cx);
|
|
||||||
})
|
|
||||||
.with_cursor_style(CursorStyle::PointingHand),
|
|
||||||
)
|
|
||||||
.with_spacing(14.0)
|
|
||||||
.with_child(
|
|
||||||
MouseEventHandler::new::<TogglePublic, _>(1, cx, move |state, _| {
|
|
||||||
let style = theme.channel_link.style_for(state);
|
|
||||||
Label::new(format!("{}", "copy link"), style.text.clone())
|
|
||||||
.contained()
|
|
||||||
.with_style(style.container.clone())
|
|
||||||
})
|
|
||||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
|
||||||
if let Some(channel) =
|
|
||||||
this.channel_store.read(cx).channel_for_id(channel_id)
|
|
||||||
{
|
|
||||||
let item = ClipboardItem::new(channel.link());
|
|
||||||
cx.write_to_clipboard(item);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.with_cursor_style(CursorStyle::PointingHand),
|
|
||||||
)
|
|
||||||
.into_any()
|
|
||||||
}
|
|
||||||
|
|
||||||
Flex::column()
|
|
||||||
.with_child(
|
|
||||||
Flex::column()
|
|
||||||
.with_child(
|
|
||||||
Label::new(format!("#{}", channel.name), theme.title.text.clone())
|
|
||||||
.contained()
|
|
||||||
.with_style(theme.title.container.clone()),
|
|
||||||
)
|
|
||||||
.with_child(render_visibility(channel.id, channel.visibility, theme, cx))
|
|
||||||
.with_child(Flex::row().with_children([
|
|
||||||
render_mode_button::<InviteMembers>(
|
|
||||||
Mode::InviteMembers,
|
|
||||||
"Invite members",
|
|
||||||
mode,
|
|
||||||
theme,
|
|
||||||
cx,
|
|
||||||
),
|
|
||||||
render_mode_button::<ManageMembers>(
|
|
||||||
Mode::ManageMembers,
|
|
||||||
"Manage members",
|
|
||||||
mode,
|
|
||||||
theme,
|
|
||||||
cx,
|
|
||||||
),
|
|
||||||
]))
|
|
||||||
.expanded()
|
|
||||||
.contained()
|
|
||||||
.with_style(theme.header),
|
|
||||||
)
|
|
||||||
.with_child(
|
|
||||||
ChildView::new(&self.picker, cx)
|
|
||||||
.contained()
|
|
||||||
.with_style(theme.body),
|
|
||||||
)
|
|
||||||
.constrained()
|
|
||||||
.with_max_height(theme.max_height)
|
|
||||||
.with_max_width(theme.max_width)
|
|
||||||
.contained()
|
|
||||||
.with_style(theme.modal)
|
|
||||||
.into_any()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
|
|
||||||
self.has_focus = true;
|
|
||||||
if cx.is_self_focused() {
|
|
||||||
cx.focus(&self.picker)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
|
impl Render for ChannelModal {
|
||||||
self.has_focus = false;
|
type Element = Div;
|
||||||
}
|
|
||||||
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||||
|
v_stack().min_w_96().child(self.picker.clone())
|
||||||
|
// let theme = &theme::current(cx).collab_panel.tabbed_modal;
|
||||||
|
|
||||||
|
// let mode = self.picker.read(cx).delegate().mode;
|
||||||
|
// let Some(channel) = self.channel_store.read(cx).channel_for_id(self.channel_id) else {
|
||||||
|
// return Empty::new().into_any();
|
||||||
|
// };
|
||||||
|
|
||||||
|
// enum InviteMembers {}
|
||||||
|
// enum ManageMembers {}
|
||||||
|
|
||||||
|
// fn render_mode_button<T: 'static>(
|
||||||
|
// mode: Mode,
|
||||||
|
// text: &'static str,
|
||||||
|
// current_mode: Mode,
|
||||||
|
// theme: &theme::TabbedModal,
|
||||||
|
// cx: &mut ViewContext<ChannelModal>,
|
||||||
|
// ) -> AnyElement<ChannelModal> {
|
||||||
|
// let active = mode == current_mode;
|
||||||
|
// MouseEventHandler::new::<T, _>(0, cx, move |state, _| {
|
||||||
|
// let contained_text = theme.tab_button.style_for(active, state);
|
||||||
|
// Label::new(text, contained_text.text.clone())
|
||||||
|
// .contained()
|
||||||
|
// .with_style(contained_text.container.clone())
|
||||||
|
// })
|
||||||
|
// .on_click(MouseButton::Left, move |_, this, cx| {
|
||||||
|
// if !active {
|
||||||
|
// this.set_mode(mode, cx);
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// .with_cursor_style(CursorStyle::PointingHand)
|
||||||
|
// .into_any()
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn render_visibility(
|
||||||
|
// channel_id: ChannelId,
|
||||||
|
// visibility: ChannelVisibility,
|
||||||
|
// theme: &theme::TabbedModal,
|
||||||
|
// cx: &mut ViewContext<ChannelModal>,
|
||||||
|
// ) -> AnyElement<ChannelModal> {
|
||||||
|
// enum TogglePublic {}
|
||||||
|
|
||||||
|
// if visibility == ChannelVisibility::Members {
|
||||||
|
// return Flex::row()
|
||||||
|
// .with_child(
|
||||||
|
// MouseEventHandler::new::<TogglePublic, _>(0, cx, move |state, _| {
|
||||||
|
// let style = theme.visibility_toggle.style_for(state);
|
||||||
|
// Label::new(format!("{}", "Public access: OFF"), style.text.clone())
|
||||||
|
// .contained()
|
||||||
|
// .with_style(style.container.clone())
|
||||||
|
// })
|
||||||
|
// .on_click(MouseButton::Left, move |_, this, cx| {
|
||||||
|
// this.channel_store
|
||||||
|
// .update(cx, |channel_store, cx| {
|
||||||
|
// channel_store.set_channel_visibility(
|
||||||
|
// channel_id,
|
||||||
|
// ChannelVisibility::Public,
|
||||||
|
// cx,
|
||||||
|
// )
|
||||||
|
// })
|
||||||
|
// .detach_and_log_err(cx);
|
||||||
|
// })
|
||||||
|
// .with_cursor_style(CursorStyle::PointingHand),
|
||||||
|
// )
|
||||||
|
// .into_any();
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Flex::row()
|
||||||
|
// .with_child(
|
||||||
|
// MouseEventHandler::new::<TogglePublic, _>(0, cx, move |state, _| {
|
||||||
|
// let style = theme.visibility_toggle.style_for(state);
|
||||||
|
// Label::new(format!("{}", "Public access: ON"), style.text.clone())
|
||||||
|
// .contained()
|
||||||
|
// .with_style(style.container.clone())
|
||||||
|
// })
|
||||||
|
// .on_click(MouseButton::Left, move |_, this, cx| {
|
||||||
|
// this.channel_store
|
||||||
|
// .update(cx, |channel_store, cx| {
|
||||||
|
// channel_store.set_channel_visibility(
|
||||||
|
// channel_id,
|
||||||
|
// ChannelVisibility::Members,
|
||||||
|
// cx,
|
||||||
|
// )
|
||||||
|
// })
|
||||||
|
// .detach_and_log_err(cx);
|
||||||
|
// })
|
||||||
|
// .with_cursor_style(CursorStyle::PointingHand),
|
||||||
|
// )
|
||||||
|
// .with_spacing(14.0)
|
||||||
|
// .with_child(
|
||||||
|
// MouseEventHandler::new::<TogglePublic, _>(1, cx, move |state, _| {
|
||||||
|
// let style = theme.channel_link.style_for(state);
|
||||||
|
// Label::new(format!("{}", "copy link"), style.text.clone())
|
||||||
|
// .contained()
|
||||||
|
// .with_style(style.container.clone())
|
||||||
|
// })
|
||||||
|
// .on_click(MouseButton::Left, move |_, this, cx| {
|
||||||
|
// if let Some(channel) =
|
||||||
|
// this.channel_store.read(cx).channel_for_id(channel_id)
|
||||||
|
// {
|
||||||
|
// let item = ClipboardItem::new(channel.link());
|
||||||
|
// cx.write_to_clipboard(item);
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// .with_cursor_style(CursorStyle::PointingHand),
|
||||||
|
// )
|
||||||
|
// .into_any()
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Flex::column()
|
||||||
|
// .with_child(
|
||||||
|
// Flex::column()
|
||||||
|
// .with_child(
|
||||||
|
// Label::new(format!("#{}", channel.name), theme.title.text.clone())
|
||||||
|
// .contained()
|
||||||
|
// .with_style(theme.title.container.clone()),
|
||||||
|
// )
|
||||||
|
// .with_child(render_visibility(channel.id, channel.visibility, theme, cx))
|
||||||
|
// .with_child(Flex::row().with_children([
|
||||||
|
// render_mode_button::<InviteMembers>(
|
||||||
|
// Mode::InviteMembers,
|
||||||
|
// "Invite members",
|
||||||
|
// mode,
|
||||||
|
// theme,
|
||||||
|
// cx,
|
||||||
|
// ),
|
||||||
|
// render_mode_button::<ManageMembers>(
|
||||||
|
// Mode::ManageMembers,
|
||||||
|
// "Manage members",
|
||||||
|
// mode,
|
||||||
|
// theme,
|
||||||
|
// cx,
|
||||||
|
// ),
|
||||||
|
// ]))
|
||||||
|
// .expanded()
|
||||||
|
// .contained()
|
||||||
|
// .with_style(theme.header),
|
||||||
|
// )
|
||||||
|
// .with_child(
|
||||||
|
// ChildView::new(&self.picker, cx)
|
||||||
|
// .contained()
|
||||||
|
// .with_style(theme.body),
|
||||||
|
// )
|
||||||
|
// .constrained()
|
||||||
|
// .with_max_height(theme.max_height)
|
||||||
|
// .with_max_width(theme.max_width)
|
||||||
|
// .contained()
|
||||||
|
// .with_style(theme.modal)
|
||||||
|
// .into_any()
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Modal for ChannelModal {
|
// fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||||
fn has_focus(&self) -> bool {
|
// self.has_focus = true;
|
||||||
self.has_focus
|
// if cx.is_self_focused() {
|
||||||
}
|
// cx.focus(&self.picker)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
fn dismiss_on_event(event: &Self::Event) -> bool {
|
// fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
|
||||||
match event {
|
// self.has_focus = false;
|
||||||
PickerEvent::Dismiss => true,
|
// }
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, PartialEq)]
|
#[derive(Copy, Clone, PartialEq)]
|
||||||
|
@ -337,19 +321,22 @@ pub enum Mode {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ChannelModalDelegate {
|
pub struct ChannelModalDelegate {
|
||||||
|
channel_modal: WeakView<ChannelModal>,
|
||||||
matching_users: Vec<Arc<User>>,
|
matching_users: Vec<Arc<User>>,
|
||||||
matching_member_indices: Vec<usize>,
|
matching_member_indices: Vec<usize>,
|
||||||
user_store: ModelHandle<UserStore>,
|
user_store: Model<UserStore>,
|
||||||
channel_store: ModelHandle<ChannelStore>,
|
channel_store: Model<ChannelStore>,
|
||||||
channel_id: ChannelId,
|
channel_id: ChannelId,
|
||||||
selected_index: usize,
|
selected_index: usize,
|
||||||
mode: Mode,
|
mode: Mode,
|
||||||
match_candidates: Vec<StringMatchCandidate>,
|
match_candidates: Vec<StringMatchCandidate>,
|
||||||
members: Vec<ChannelMembership>,
|
members: Vec<ChannelMembership>,
|
||||||
context_menu: ViewHandle<ContextMenu>,
|
// context_menu: ViewHandle<ContextMenu>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PickerDelegate for ChannelModalDelegate {
|
impl PickerDelegate for ChannelModalDelegate {
|
||||||
|
type ListItem = Div;
|
||||||
|
|
||||||
fn placeholder_text(&self) -> Arc<str> {
|
fn placeholder_text(&self) -> Arc<str> {
|
||||||
"Search collaborator by username...".into()
|
"Search collaborator by username...".into()
|
||||||
}
|
}
|
||||||
|
@ -382,19 +369,19 @@ impl PickerDelegate for ChannelModalDelegate {
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let matches = cx.background().block(match_strings(
|
let matches = cx.background_executor().block(match_strings(
|
||||||
&self.match_candidates,
|
&self.match_candidates,
|
||||||
&query,
|
&query,
|
||||||
true,
|
true,
|
||||||
usize::MAX,
|
usize::MAX,
|
||||||
&Default::default(),
|
&Default::default(),
|
||||||
cx.background().clone(),
|
cx.background_executor().clone(),
|
||||||
));
|
));
|
||||||
|
|
||||||
cx.spawn(|picker, mut cx| async move {
|
cx.spawn(|picker, mut cx| async move {
|
||||||
picker
|
picker
|
||||||
.update(&mut cx, |picker, cx| {
|
.update(&mut cx, |picker, cx| {
|
||||||
let delegate = picker.delegate_mut();
|
let delegate = &mut picker.delegate;
|
||||||
delegate.matching_member_indices.clear();
|
delegate.matching_member_indices.clear();
|
||||||
delegate
|
delegate
|
||||||
.matching_member_indices
|
.matching_member_indices
|
||||||
|
@ -412,8 +399,7 @@ impl PickerDelegate for ChannelModalDelegate {
|
||||||
async {
|
async {
|
||||||
let users = search_users.await?;
|
let users = search_users.await?;
|
||||||
picker.update(&mut cx, |picker, cx| {
|
picker.update(&mut cx, |picker, cx| {
|
||||||
let delegate = picker.delegate_mut();
|
picker.delegate.matching_users = users;
|
||||||
delegate.matching_users = users;
|
|
||||||
cx.notify();
|
cx.notify();
|
||||||
})?;
|
})?;
|
||||||
anyhow::Ok(())
|
anyhow::Ok(())
|
||||||
|
@ -445,138 +431,142 @@ impl PickerDelegate for ChannelModalDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
|
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
|
||||||
cx.emit(PickerEvent::Dismiss);
|
self.channel_modal
|
||||||
|
.update(cx, |_, cx| {
|
||||||
|
cx.emit(DismissEvent);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_match(
|
fn render_match(
|
||||||
&self,
|
&self,
|
||||||
ix: usize,
|
ix: usize,
|
||||||
mouse_state: &mut MouseState,
|
|
||||||
selected: bool,
|
selected: bool,
|
||||||
cx: &gpui::AppContext,
|
cx: &mut ViewContext<Picker<Self>>,
|
||||||
) -> AnyElement<Picker<Self>> {
|
) -> Option<Self::ListItem> {
|
||||||
let full_theme = &theme::current(cx);
|
|
||||||
let theme = &full_theme.collab_panel.channel_modal;
|
|
||||||
let tabbed_modal = &full_theme.collab_panel.tabbed_modal;
|
|
||||||
let (user, role) = self.user_at_index(ix).unwrap();
|
|
||||||
let request_status = self.member_status(user.id, cx);
|
|
||||||
|
|
||||||
let style = tabbed_modal
|
|
||||||
.picker
|
|
||||||
.item
|
|
||||||
.in_state(selected)
|
|
||||||
.style_for(mouse_state);
|
|
||||||
|
|
||||||
let in_manage = matches!(self.mode, Mode::ManageMembers);
|
|
||||||
|
|
||||||
let mut result = Flex::row()
|
|
||||||
.with_children(user.avatar.clone().map(|avatar| {
|
|
||||||
Image::from_data(avatar)
|
|
||||||
.with_style(theme.contact_avatar)
|
|
||||||
.aligned()
|
|
||||||
.left()
|
|
||||||
}))
|
|
||||||
.with_child(
|
|
||||||
Label::new(user.github_login.clone(), style.label.clone())
|
|
||||||
.contained()
|
|
||||||
.with_style(theme.contact_username)
|
|
||||||
.aligned()
|
|
||||||
.left(),
|
|
||||||
)
|
|
||||||
.with_children({
|
|
||||||
(in_manage && request_status == Some(proto::channel_member::Kind::Invitee)).then(
|
|
||||||
|| {
|
|
||||||
Label::new("Invited", theme.member_tag.text.clone())
|
|
||||||
.contained()
|
|
||||||
.with_style(theme.member_tag.container)
|
|
||||||
.aligned()
|
|
||||||
.left()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.with_children(if in_manage && role == Some(ChannelRole::Admin) {
|
|
||||||
Some(
|
|
||||||
Label::new("Admin", theme.member_tag.text.clone())
|
|
||||||
.contained()
|
|
||||||
.with_style(theme.member_tag.container)
|
|
||||||
.aligned()
|
|
||||||
.left(),
|
|
||||||
)
|
|
||||||
} else if in_manage && role == Some(ChannelRole::Guest) {
|
|
||||||
Some(
|
|
||||||
Label::new("Guest", theme.member_tag.text.clone())
|
|
||||||
.contained()
|
|
||||||
.with_style(theme.member_tag.container)
|
|
||||||
.aligned()
|
|
||||||
.left(),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
None
|
None
|
||||||
})
|
// let full_theme = &theme::current(cx);
|
||||||
.with_children({
|
// let theme = &full_theme.collab_panel.channel_modal;
|
||||||
let svg = match self.mode {
|
// let tabbed_modal = &full_theme.collab_panel.tabbed_modal;
|
||||||
Mode::ManageMembers => Some(
|
// let (user, role) = self.user_at_index(ix).unwrap();
|
||||||
Svg::new("icons/ellipsis.svg")
|
// let request_status = self.member_status(user.id, cx);
|
||||||
.with_color(theme.member_icon.color)
|
|
||||||
.constrained()
|
|
||||||
.with_width(theme.member_icon.icon_width)
|
|
||||||
.aligned()
|
|
||||||
.constrained()
|
|
||||||
.with_width(theme.member_icon.button_width)
|
|
||||||
.with_height(theme.member_icon.button_width)
|
|
||||||
.contained()
|
|
||||||
.with_style(theme.member_icon.container),
|
|
||||||
),
|
|
||||||
Mode::InviteMembers => match request_status {
|
|
||||||
Some(proto::channel_member::Kind::Member) => Some(
|
|
||||||
Svg::new("icons/check.svg")
|
|
||||||
.with_color(theme.member_icon.color)
|
|
||||||
.constrained()
|
|
||||||
.with_width(theme.member_icon.icon_width)
|
|
||||||
.aligned()
|
|
||||||
.constrained()
|
|
||||||
.with_width(theme.member_icon.button_width)
|
|
||||||
.with_height(theme.member_icon.button_width)
|
|
||||||
.contained()
|
|
||||||
.with_style(theme.member_icon.container),
|
|
||||||
),
|
|
||||||
Some(proto::channel_member::Kind::Invitee) => Some(
|
|
||||||
Svg::new("icons/check.svg")
|
|
||||||
.with_color(theme.invitee_icon.color)
|
|
||||||
.constrained()
|
|
||||||
.with_width(theme.invitee_icon.icon_width)
|
|
||||||
.aligned()
|
|
||||||
.constrained()
|
|
||||||
.with_width(theme.invitee_icon.button_width)
|
|
||||||
.with_height(theme.invitee_icon.button_width)
|
|
||||||
.contained()
|
|
||||||
.with_style(theme.invitee_icon.container),
|
|
||||||
),
|
|
||||||
Some(proto::channel_member::Kind::AncestorMember) | None => None,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
svg.map(|svg| svg.aligned().flex_float().into_any())
|
// let style = tabbed_modal
|
||||||
})
|
// .picker
|
||||||
.contained()
|
// .item
|
||||||
.with_style(style.container)
|
// .in_state(selected)
|
||||||
.constrained()
|
// .style_for(mouse_state);
|
||||||
.with_height(tabbed_modal.row_height)
|
|
||||||
.into_any();
|
|
||||||
|
|
||||||
if selected {
|
// let in_manage = matches!(self.mode, Mode::ManageMembers);
|
||||||
result = Stack::new()
|
|
||||||
.with_child(result)
|
|
||||||
.with_child(
|
|
||||||
ChildView::new(&self.context_menu, cx)
|
|
||||||
.aligned()
|
|
||||||
.top()
|
|
||||||
.right(),
|
|
||||||
)
|
|
||||||
.into_any();
|
|
||||||
}
|
|
||||||
|
|
||||||
result
|
// let mut result = Flex::row()
|
||||||
|
// .with_children(user.avatar.clone().map(|avatar| {
|
||||||
|
// Image::from_data(avatar)
|
||||||
|
// .with_style(theme.contact_avatar)
|
||||||
|
// .aligned()
|
||||||
|
// .left()
|
||||||
|
// }))
|
||||||
|
// .with_child(
|
||||||
|
// Label::new(user.github_login.clone(), style.label.clone())
|
||||||
|
// .contained()
|
||||||
|
// .with_style(theme.contact_username)
|
||||||
|
// .aligned()
|
||||||
|
// .left(),
|
||||||
|
// )
|
||||||
|
// .with_children({
|
||||||
|
// (in_manage && request_status == Some(proto::channel_member::Kind::Invitee)).then(
|
||||||
|
// || {
|
||||||
|
// Label::new("Invited", theme.member_tag.text.clone())
|
||||||
|
// .contained()
|
||||||
|
// .with_style(theme.member_tag.container)
|
||||||
|
// .aligned()
|
||||||
|
// .left()
|
||||||
|
// },
|
||||||
|
// )
|
||||||
|
// })
|
||||||
|
// .with_children(if in_manage && role == Some(ChannelRole::Admin) {
|
||||||
|
// Some(
|
||||||
|
// Label::new("Admin", theme.member_tag.text.clone())
|
||||||
|
// .contained()
|
||||||
|
// .with_style(theme.member_tag.container)
|
||||||
|
// .aligned()
|
||||||
|
// .left(),
|
||||||
|
// )
|
||||||
|
// } else if in_manage && role == Some(ChannelRole::Guest) {
|
||||||
|
// Some(
|
||||||
|
// Label::new("Guest", theme.member_tag.text.clone())
|
||||||
|
// .contained()
|
||||||
|
// .with_style(theme.member_tag.container)
|
||||||
|
// .aligned()
|
||||||
|
// .left(),
|
||||||
|
// )
|
||||||
|
// } else {
|
||||||
|
// None
|
||||||
|
// })
|
||||||
|
// .with_children({
|
||||||
|
// let svg = match self.mode {
|
||||||
|
// Mode::ManageMembers => Some(
|
||||||
|
// Svg::new("icons/ellipsis.svg")
|
||||||
|
// .with_color(theme.member_icon.color)
|
||||||
|
// .constrained()
|
||||||
|
// .with_width(theme.member_icon.icon_width)
|
||||||
|
// .aligned()
|
||||||
|
// .constrained()
|
||||||
|
// .with_width(theme.member_icon.button_width)
|
||||||
|
// .with_height(theme.member_icon.button_width)
|
||||||
|
// .contained()
|
||||||
|
// .with_style(theme.member_icon.container),
|
||||||
|
// ),
|
||||||
|
// Mode::InviteMembers => match request_status {
|
||||||
|
// Some(proto::channel_member::Kind::Member) => Some(
|
||||||
|
// Svg::new("icons/check.svg")
|
||||||
|
// .with_color(theme.member_icon.color)
|
||||||
|
// .constrained()
|
||||||
|
// .with_width(theme.member_icon.icon_width)
|
||||||
|
// .aligned()
|
||||||
|
// .constrained()
|
||||||
|
// .with_width(theme.member_icon.button_width)
|
||||||
|
// .with_height(theme.member_icon.button_width)
|
||||||
|
// .contained()
|
||||||
|
// .with_style(theme.member_icon.container),
|
||||||
|
// ),
|
||||||
|
// Some(proto::channel_member::Kind::Invitee) => Some(
|
||||||
|
// Svg::new("icons/check.svg")
|
||||||
|
// .with_color(theme.invitee_icon.color)
|
||||||
|
// .constrained()
|
||||||
|
// .with_width(theme.invitee_icon.icon_width)
|
||||||
|
// .aligned()
|
||||||
|
// .constrained()
|
||||||
|
// .with_width(theme.invitee_icon.button_width)
|
||||||
|
// .with_height(theme.invitee_icon.button_width)
|
||||||
|
// .contained()
|
||||||
|
// .with_style(theme.invitee_icon.container),
|
||||||
|
// ),
|
||||||
|
// Some(proto::channel_member::Kind::AncestorMember) | None => None,
|
||||||
|
// },
|
||||||
|
// };
|
||||||
|
|
||||||
|
// svg.map(|svg| svg.aligned().flex_float().into_any())
|
||||||
|
// })
|
||||||
|
// .contained()
|
||||||
|
// .with_style(style.container)
|
||||||
|
// .constrained()
|
||||||
|
// .with_height(tabbed_modal.row_height)
|
||||||
|
// .into_any();
|
||||||
|
|
||||||
|
// if selected {
|
||||||
|
// result = Stack::new()
|
||||||
|
// .with_child(result)
|
||||||
|
// .with_child(
|
||||||
|
// ChildView::new(&self.context_menu, cx)
|
||||||
|
// .aligned()
|
||||||
|
// .top()
|
||||||
|
// .right(),
|
||||||
|
// )
|
||||||
|
// .into_any();
|
||||||
|
// }
|
||||||
|
|
||||||
|
// result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -623,7 +613,7 @@ impl ChannelModalDelegate {
|
||||||
cx.spawn(|picker, mut cx| async move {
|
cx.spawn(|picker, mut cx| async move {
|
||||||
update.await?;
|
update.await?;
|
||||||
picker.update(&mut cx, |picker, cx| {
|
picker.update(&mut cx, |picker, cx| {
|
||||||
let this = picker.delegate_mut();
|
let this = &mut picker.delegate;
|
||||||
if let Some(member) = this.members.iter_mut().find(|m| m.user.id == user.id) {
|
if let Some(member) = this.members.iter_mut().find(|m| m.user.id == user.id) {
|
||||||
member.role = new_role;
|
member.role = new_role;
|
||||||
}
|
}
|
||||||
|
@ -644,7 +634,7 @@ impl ChannelModalDelegate {
|
||||||
cx.spawn(|picker, mut cx| async move {
|
cx.spawn(|picker, mut cx| async move {
|
||||||
update.await?;
|
update.await?;
|
||||||
picker.update(&mut cx, |picker, cx| {
|
picker.update(&mut cx, |picker, cx| {
|
||||||
let this = picker.delegate_mut();
|
let this = &mut picker.delegate;
|
||||||
if let Some(ix) = this.members.iter_mut().position(|m| m.user.id == user_id) {
|
if let Some(ix) = this.members.iter_mut().position(|m| m.user.id == user_id) {
|
||||||
this.members.remove(ix);
|
this.members.remove(ix);
|
||||||
this.matching_member_indices.retain_mut(|member_ix| {
|
this.matching_member_indices.retain_mut(|member_ix| {
|
||||||
|
@ -683,7 +673,7 @@ impl ChannelModalDelegate {
|
||||||
kind: proto::channel_member::Kind::Invitee,
|
kind: proto::channel_member::Kind::Invitee,
|
||||||
role: ChannelRole::Member,
|
role: ChannelRole::Member,
|
||||||
};
|
};
|
||||||
let members = &mut this.delegate_mut().members;
|
let members = &mut this.delegate.members;
|
||||||
match members.binary_search_by_key(&new_member.sort_key(), |k| k.sort_key()) {
|
match members.binary_search_by_key(&new_member.sort_key(), |k| k.sort_key()) {
|
||||||
Ok(ix) | Err(ix) => members.insert(ix, new_member),
|
Ok(ix) | Err(ix) => members.insert(ix, new_member),
|
||||||
}
|
}
|
||||||
|
@ -695,23 +685,23 @@ impl ChannelModalDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn show_context_menu(&mut self, role: ChannelRole, cx: &mut ViewContext<Picker<Self>>) {
|
fn show_context_menu(&mut self, role: ChannelRole, cx: &mut ViewContext<Picker<Self>>) {
|
||||||
self.context_menu.update(cx, |context_menu, cx| {
|
// self.context_menu.update(cx, |context_menu, cx| {
|
||||||
context_menu.show(
|
// context_menu.show(
|
||||||
Default::default(),
|
// Default::default(),
|
||||||
AnchorCorner::TopRight,
|
// AnchorCorner::TopRight,
|
||||||
vec![
|
// vec![
|
||||||
ContextMenuItem::action("Remove", RemoveMember),
|
// ContextMenuItem::action("Remove", RemoveMember),
|
||||||
ContextMenuItem::action(
|
// ContextMenuItem::action(
|
||||||
if role == ChannelRole::Admin {
|
// if role == ChannelRole::Admin {
|
||||||
"Make non-admin"
|
// "Make non-admin"
|
||||||
} else {
|
// } else {
|
||||||
"Make admin"
|
// "Make admin"
|
||||||
},
|
// },
|
||||||
ToggleMemberAdmin,
|
// ToggleMemberAdmin,
|
||||||
),
|
// ),
|
||||||
],
|
// ],
|
||||||
cx,
|
// cx,
|
||||||
)
|
// )
|
||||||
})
|
// })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,16 +35,19 @@ use gpui::{
|
||||||
ParentElement, Render, RenderOnce, Stateful, StatefulInteractiveElement, Styled, Subscription,
|
ParentElement, Render, RenderOnce, Stateful, StatefulInteractiveElement, Styled, Subscription,
|
||||||
ViewContext, VisualContext, WeakView, WindowBounds,
|
ViewContext, VisualContext, WeakView, WindowBounds,
|
||||||
};
|
};
|
||||||
use project::Project;
|
use project::{Project, RepositoryEntry};
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
use ui::{h_stack, prelude::*, Avatar, Button, ButtonStyle, IconButton, KeyBinding, Tooltip};
|
use ui::{
|
||||||
|
h_stack, popover_menu, prelude::*, Avatar, Button, ButtonLike, ButtonStyle, ContextMenu, Icon,
|
||||||
|
IconButton, IconElement, KeyBinding, Tooltip,
|
||||||
|
};
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
use workspace::{notifications::NotifyResultExt, Workspace};
|
use workspace::{notifications::NotifyResultExt, Workspace};
|
||||||
|
|
||||||
use crate::face_pile::FacePile;
|
use crate::face_pile::FacePile;
|
||||||
|
|
||||||
// const MAX_PROJECT_NAME_LENGTH: usize = 40;
|
const MAX_PROJECT_NAME_LENGTH: usize = 40;
|
||||||
// const MAX_BRANCH_NAME_LENGTH: usize = 40;
|
const MAX_BRANCH_NAME_LENGTH: usize = 40;
|
||||||
|
|
||||||
// actions!(
|
// actions!(
|
||||||
// collab,
|
// collab,
|
||||||
|
@ -100,17 +103,18 @@ impl Render for CollabTitlebarItem {
|
||||||
.update(cx, |this, cx| this.call_state().remote_participants(cx))
|
.update(cx, |this, cx| this.call_state().remote_participants(cx))
|
||||||
.log_err()
|
.log_err()
|
||||||
.flatten();
|
.flatten();
|
||||||
let mic_icon = if self
|
let is_muted = self
|
||||||
.workspace
|
.workspace
|
||||||
.update(cx, |this, cx| this.call_state().is_muted(cx))
|
.update(cx, |this, cx| this.call_state().is_muted(cx))
|
||||||
.log_err()
|
.log_err()
|
||||||
.flatten()
|
.flatten()
|
||||||
.unwrap_or_default()
|
.unwrap_or_default();
|
||||||
{
|
let is_deafened = self
|
||||||
ui::Icon::MicMute
|
.workspace
|
||||||
} else {
|
.update(cx, |this, cx| this.call_state().is_deafened(cx))
|
||||||
ui::Icon::Mic
|
.log_err()
|
||||||
};
|
.flatten()
|
||||||
|
.unwrap_or_default();
|
||||||
let speakers_icon = if self
|
let speakers_icon = if self
|
||||||
.workspace
|
.workspace
|
||||||
.update(cx, |this, cx| this.call_state().is_deafened(cx))
|
.update(cx, |this, cx| this.call_state().is_deafened(cx))
|
||||||
|
@ -146,56 +150,11 @@ impl Render for CollabTitlebarItem {
|
||||||
.child(
|
.child(
|
||||||
h_stack()
|
h_stack()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
// TODO - Add player menu
|
.when(is_in_room, |this| {
|
||||||
.child(
|
this.children(self.render_project_owner(cx))
|
||||||
div()
|
|
||||||
.border()
|
|
||||||
.border_color(gpui::red())
|
|
||||||
.id("project_owner_indicator")
|
|
||||||
.child(
|
|
||||||
Button::new("player", "player")
|
|
||||||
.style(ButtonStyle::Subtle)
|
|
||||||
.color(Some(Color::Player(0))),
|
|
||||||
)
|
|
||||||
.tooltip(move |cx| Tooltip::text("Toggle following", cx)),
|
|
||||||
)
|
|
||||||
// TODO - Add project menu
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.border()
|
|
||||||
.border_color(gpui::red())
|
|
||||||
.id("titlebar_project_menu_button")
|
|
||||||
.child(
|
|
||||||
Button::new("project_name", "project_name")
|
|
||||||
.style(ButtonStyle::Subtle),
|
|
||||||
)
|
|
||||||
.tooltip(move |cx| Tooltip::text("Recent Projects", cx)),
|
|
||||||
)
|
|
||||||
// TODO - Add git menu
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.border()
|
|
||||||
.border_color(gpui::red())
|
|
||||||
.id("titlebar_git_menu_button")
|
|
||||||
.child(
|
|
||||||
Button::new("branch_name", "branch_name")
|
|
||||||
.style(ButtonStyle::Subtle)
|
|
||||||
.color(Some(Color::Muted)),
|
|
||||||
)
|
|
||||||
.tooltip(move |cx| {
|
|
||||||
cx.build_view(|_| {
|
|
||||||
Tooltip::new("Recent Branches")
|
|
||||||
.key_binding(KeyBinding::new(gpui::KeyBinding::new(
|
|
||||||
"cmd-b",
|
|
||||||
// todo!() Replace with real action.
|
|
||||||
gpui::NoAction,
|
|
||||||
None,
|
|
||||||
)))
|
|
||||||
.meta("Only local branches shown")
|
|
||||||
})
|
})
|
||||||
.into()
|
.child(self.render_project_name(cx))
|
||||||
}),
|
.children(self.render_project_branch(cx)),
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.when_some(
|
.when_some(
|
||||||
users.zip(current_user.clone()),
|
users.zip(current_user.clone()),
|
||||||
|
@ -236,13 +195,21 @@ impl Render for CollabTitlebarItem {
|
||||||
.when(is_in_room, |this| {
|
.when(is_in_room, |this| {
|
||||||
this.child(
|
this.child(
|
||||||
h_stack()
|
h_stack()
|
||||||
|
.gap_1()
|
||||||
.child(
|
.child(
|
||||||
h_stack()
|
h_stack()
|
||||||
.child(Button::new(
|
.gap_1()
|
||||||
|
.child(
|
||||||
|
Button::new(
|
||||||
"toggle_sharing",
|
"toggle_sharing",
|
||||||
if is_shared { "Unshare" } else { "Share" },
|
if is_shared { "Unshare" } else { "Share" },
|
||||||
))
|
)
|
||||||
.child(IconButton::new("leave-call", ui::Icon::Exit).on_click({
|
.style(ButtonStyle::Subtle),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
IconButton::new("leave-call", ui::Icon::Exit)
|
||||||
|
.style(ButtonStyle::Subtle)
|
||||||
|
.on_click({
|
||||||
let workspace = workspace.clone();
|
let workspace = workspace.clone();
|
||||||
move |_, cx| {
|
move |_, cx| {
|
||||||
workspace
|
workspace
|
||||||
|
@ -251,11 +218,24 @@ impl Render for CollabTitlebarItem {
|
||||||
})
|
})
|
||||||
.log_err();
|
.log_err();
|
||||||
}
|
}
|
||||||
})),
|
}),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
h_stack()
|
h_stack()
|
||||||
.child(IconButton::new("mute-microphone", mic_icon).on_click({
|
.gap_1()
|
||||||
|
.child(
|
||||||
|
IconButton::new(
|
||||||
|
"mute-microphone",
|
||||||
|
if is_muted {
|
||||||
|
ui::Icon::MicMute
|
||||||
|
} else {
|
||||||
|
ui::Icon::Mic
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.style(ButtonStyle::Subtle)
|
||||||
|
.selected(is_muted)
|
||||||
|
.on_click({
|
||||||
let workspace = workspace.clone();
|
let workspace = workspace.clone();
|
||||||
move |_, cx| {
|
move |_, cx| {
|
||||||
workspace
|
workspace
|
||||||
|
@ -264,8 +244,21 @@ impl Render for CollabTitlebarItem {
|
||||||
})
|
})
|
||||||
.log_err();
|
.log_err();
|
||||||
}
|
}
|
||||||
}))
|
}),
|
||||||
.child(IconButton::new("mute-sound", speakers_icon).on_click({
|
)
|
||||||
|
.child(
|
||||||
|
IconButton::new("mute-sound", speakers_icon)
|
||||||
|
.style(ButtonStyle::Subtle)
|
||||||
|
.selected(is_deafened.clone())
|
||||||
|
.tooltip(move |cx| {
|
||||||
|
Tooltip::with_meta(
|
||||||
|
"Deafen Audio",
|
||||||
|
None,
|
||||||
|
"Mic will be muted",
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.on_click({
|
||||||
let workspace = workspace.clone();
|
let workspace = workspace.clone();
|
||||||
move |_, cx| {
|
move |_, cx| {
|
||||||
workspace
|
workspace
|
||||||
|
@ -274,24 +267,54 @@ impl Render for CollabTitlebarItem {
|
||||||
})
|
})
|
||||||
.log_err();
|
.log_err();
|
||||||
}
|
}
|
||||||
}))
|
}),
|
||||||
.child(IconButton::new("screen-share", ui::Icon::Screen).on_click(
|
)
|
||||||
move |_, cx| {
|
.child(
|
||||||
|
IconButton::new("screen-share", ui::Icon::Screen)
|
||||||
|
.style(ButtonStyle::Subtle)
|
||||||
|
.on_click(move |_, cx| {
|
||||||
workspace
|
workspace
|
||||||
.update(cx, |this, cx| {
|
.update(cx, |this, cx| {
|
||||||
this.call_state().toggle_screen_share(cx);
|
this.call_state().toggle_screen_share(cx);
|
||||||
})
|
})
|
||||||
.log_err();
|
.log_err();
|
||||||
},
|
}),
|
||||||
))
|
)
|
||||||
.pl_2(),
|
.pl_2(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.map(|this| {
|
.child(h_stack().px_1p5().map(|this| {
|
||||||
if let Some(user) = current_user {
|
if let Some(user) = current_user {
|
||||||
this.when_some(user.avatar.clone(), |this, avatar| {
|
this.when_some(user.avatar.clone(), |this, avatar| {
|
||||||
this.child(ui::Avatar::data(avatar))
|
// TODO: Finish implementing user menu popover
|
||||||
|
//
|
||||||
|
this.child(
|
||||||
|
popover_menu("user-menu")
|
||||||
|
.menu(|cx| ContextMenu::build(cx, |menu, _| menu.header("ADADA")))
|
||||||
|
.trigger(
|
||||||
|
ButtonLike::new("user-menu")
|
||||||
|
.child(
|
||||||
|
h_stack().gap_0p5().child(Avatar::data(avatar)).child(
|
||||||
|
IconElement::new(Icon::ChevronDown)
|
||||||
|
.color(Color::Muted),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.style(ButtonStyle::Subtle)
|
||||||
|
.tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)),
|
||||||
|
)
|
||||||
|
.anchor(gpui::AnchorCorner::TopRight),
|
||||||
|
)
|
||||||
|
// this.child(
|
||||||
|
// ButtonLike::new("user-menu")
|
||||||
|
// .child(
|
||||||
|
// h_stack().gap_0p5().child(Avatar::data(avatar)).child(
|
||||||
|
// IconElement::new(Icon::ChevronDown).color(Color::Muted),
|
||||||
|
// ),
|
||||||
|
// )
|
||||||
|
// .style(ButtonStyle::Subtle)
|
||||||
|
// .tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)),
|
||||||
|
// )
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
this.child(Button::new("sign_in", "Sign in").on_click(move |_, cx| {
|
this.child(Button::new("sign_in", "Sign in").on_click(move |_, cx| {
|
||||||
|
@ -305,7 +328,7 @@ impl Render for CollabTitlebarItem {
|
||||||
.detach();
|
.detach();
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
})
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -424,6 +447,110 @@ impl CollabTitlebarItem {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// resolve if you are in a room -> render_project_owner
|
||||||
|
// render_project_owner -> resolve if you are in a room -> Option<foo>
|
||||||
|
|
||||||
|
pub fn render_project_owner(&self, cx: &mut ViewContext<Self>) -> Option<impl Element> {
|
||||||
|
// TODO: We can't finish implementing this until project sharing works
|
||||||
|
// - [ ] Show the project owner when the project is remote (maybe done)
|
||||||
|
// - [x] Show the project owner when the project is local
|
||||||
|
// - [ ] Show the project owner with a lock icon when the project is local and unshared
|
||||||
|
|
||||||
|
let remote_id = self.project.read(cx).remote_id();
|
||||||
|
let is_local = remote_id.is_none();
|
||||||
|
let is_shared = self.project.read(cx).is_shared();
|
||||||
|
let (user_name, participant_index) = {
|
||||||
|
if let Some(host) = self.project.read(cx).host() {
|
||||||
|
debug_assert!(!is_local);
|
||||||
|
let (Some(host_user), Some(participant_index)) = (
|
||||||
|
self.user_store.read(cx).get_cached_user(host.user_id),
|
||||||
|
self.user_store
|
||||||
|
.read(cx)
|
||||||
|
.participant_indices()
|
||||||
|
.get(&host.user_id),
|
||||||
|
) else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
(host_user.github_login.clone(), participant_index.0)
|
||||||
|
} else {
|
||||||
|
debug_assert!(is_local);
|
||||||
|
let name = self
|
||||||
|
.user_store
|
||||||
|
.read(cx)
|
||||||
|
.current_user()
|
||||||
|
.map(|user| user.github_login.clone())?;
|
||||||
|
(name, 0)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Some(
|
||||||
|
div().border().border_color(gpui::red()).child(
|
||||||
|
Button::new(
|
||||||
|
"project_owner_trigger",
|
||||||
|
format!("{user_name} ({})", !is_shared),
|
||||||
|
)
|
||||||
|
.color(Color::Player(participant_index))
|
||||||
|
.style(ButtonStyle::Subtle)
|
||||||
|
.tooltip(move |cx| Tooltip::text("Toggle following", cx)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_project_name(&self, cx: &mut ViewContext<Self>) -> impl Element {
|
||||||
|
let name = {
|
||||||
|
let mut names = self.project.read(cx).visible_worktrees(cx).map(|worktree| {
|
||||||
|
let worktree = worktree.read(cx);
|
||||||
|
worktree.root_name()
|
||||||
|
});
|
||||||
|
|
||||||
|
names.next().unwrap_or("")
|
||||||
|
};
|
||||||
|
|
||||||
|
let name = util::truncate_and_trailoff(name, MAX_PROJECT_NAME_LENGTH);
|
||||||
|
|
||||||
|
div().border().border_color(gpui::red()).child(
|
||||||
|
Button::new("project_name_trigger", name)
|
||||||
|
.style(ButtonStyle::Subtle)
|
||||||
|
.tooltip(move |cx| Tooltip::text("Recent Projects", cx)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_project_branch(&self, cx: &mut ViewContext<Self>) -> Option<impl Element> {
|
||||||
|
let entry = {
|
||||||
|
let mut names_and_branches =
|
||||||
|
self.project.read(cx).visible_worktrees(cx).map(|worktree| {
|
||||||
|
let worktree = worktree.read(cx);
|
||||||
|
worktree.root_git_entry()
|
||||||
|
});
|
||||||
|
|
||||||
|
names_and_branches.next().flatten()
|
||||||
|
};
|
||||||
|
|
||||||
|
let branch_name = entry
|
||||||
|
.as_ref()
|
||||||
|
.and_then(RepositoryEntry::branch)
|
||||||
|
.map(|branch| util::truncate_and_trailoff(&branch, MAX_BRANCH_NAME_LENGTH))?;
|
||||||
|
|
||||||
|
Some(
|
||||||
|
div().border().border_color(gpui::red()).child(
|
||||||
|
Button::new("project_branch_trigger", branch_name)
|
||||||
|
.style(ButtonStyle::Subtle)
|
||||||
|
.tooltip(move |cx| {
|
||||||
|
cx.build_view(|_| {
|
||||||
|
Tooltip::new("Recent Branches")
|
||||||
|
.key_binding(KeyBinding::new(gpui::KeyBinding::new(
|
||||||
|
"cmd-b",
|
||||||
|
// todo!() Replace with real action.
|
||||||
|
gpui::NoAction,
|
||||||
|
None,
|
||||||
|
)))
|
||||||
|
.meta("Local branches only")
|
||||||
|
})
|
||||||
|
.into()
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// fn collect_title_root_names(
|
// fn collect_title_root_names(
|
||||||
// &self,
|
// &self,
|
||||||
// theme: Arc<Theme>,
|
// theme: Arc<Theme>,
|
||||||
|
|
27
crates/copilot_button2/Cargo.toml
Normal file
27
crates/copilot_button2/Cargo.toml
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
[package]
|
||||||
|
name = "copilot_button2"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "src/copilot_button.rs"
|
||||||
|
doctest = false
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
copilot = { package = "copilot2", path = "../copilot2" }
|
||||||
|
editor = { package = "editor2", path = "../editor2" }
|
||||||
|
fs = { package = "fs2", path = "../fs2" }
|
||||||
|
zed-actions = { package="zed_actions2", path = "../zed_actions2"}
|
||||||
|
gpui = { package = "gpui2", path = "../gpui2" }
|
||||||
|
language = { package = "language2", path = "../language2" }
|
||||||
|
settings = { package = "settings2", path = "../settings2" }
|
||||||
|
theme = { package = "theme2", path = "../theme2" }
|
||||||
|
util = { path = "../util" }
|
||||||
|
workspace = { package = "workspace2", path = "../workspace2" }
|
||||||
|
anyhow.workspace = true
|
||||||
|
smol.workspace = true
|
||||||
|
futures.workspace = true
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
|
371
crates/copilot_button2/src/copilot_button.rs
Normal file
371
crates/copilot_button2/src/copilot_button.rs
Normal file
|
@ -0,0 +1,371 @@
|
||||||
|
#![allow(unused)]
|
||||||
|
use anyhow::Result;
|
||||||
|
use copilot::{Copilot, SignOut, Status};
|
||||||
|
use editor::{scroll::autoscroll::Autoscroll, Editor};
|
||||||
|
use fs::Fs;
|
||||||
|
use gpui::{
|
||||||
|
div, Action, AnchorCorner, AppContext, AsyncAppContext, AsyncWindowContext, Div, Entity,
|
||||||
|
ParentElement, Render, Subscription, View, ViewContext, WeakView, WindowContext,
|
||||||
|
};
|
||||||
|
use language::{
|
||||||
|
language_settings::{self, all_language_settings, AllLanguageSettings},
|
||||||
|
File, Language,
|
||||||
|
};
|
||||||
|
use settings::{update_settings_file, Settings, SettingsStore};
|
||||||
|
use std::{path::Path, sync::Arc};
|
||||||
|
use util::{paths, ResultExt};
|
||||||
|
use workspace::{
|
||||||
|
create_and_open_local_file,
|
||||||
|
item::ItemHandle,
|
||||||
|
ui::{
|
||||||
|
popover_menu, ButtonCommon, Clickable, ContextMenu, Icon, IconButton, PopoverMenu, Tooltip,
|
||||||
|
},
|
||||||
|
StatusItemView, Toast, Workspace,
|
||||||
|
};
|
||||||
|
use zed_actions::OpenBrowser;
|
||||||
|
|
||||||
|
const COPILOT_SETTINGS_URL: &str = "https://github.com/settings/copilot";
|
||||||
|
const COPILOT_STARTING_TOAST_ID: usize = 1337;
|
||||||
|
const COPILOT_ERROR_TOAST_ID: usize = 1338;
|
||||||
|
|
||||||
|
pub struct CopilotButton {
|
||||||
|
editor_subscription: Option<(Subscription, usize)>,
|
||||||
|
editor_enabled: Option<bool>,
|
||||||
|
language: Option<Arc<Language>>,
|
||||||
|
file: Option<Arc<dyn File>>,
|
||||||
|
fs: Arc<dyn Fs>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for CopilotButton {
|
||||||
|
type Element = Div;
|
||||||
|
|
||||||
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||||
|
let all_language_settings = all_language_settings(None, cx);
|
||||||
|
if !all_language_settings.copilot.feature_enabled {
|
||||||
|
return div();
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(copilot) = Copilot::global(cx) else {
|
||||||
|
return div();
|
||||||
|
};
|
||||||
|
let status = copilot.read(cx).status();
|
||||||
|
|
||||||
|
let enabled = self
|
||||||
|
.editor_enabled
|
||||||
|
.unwrap_or_else(|| all_language_settings.copilot_enabled(None, None));
|
||||||
|
|
||||||
|
let icon = match status {
|
||||||
|
Status::Error(_) => Icon::CopilotError,
|
||||||
|
Status::Authorized => {
|
||||||
|
if enabled {
|
||||||
|
Icon::Copilot
|
||||||
|
} else {
|
||||||
|
Icon::CopilotDisabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => Icon::CopilotInit,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Status::Error(e) = status {
|
||||||
|
return div().child(
|
||||||
|
IconButton::new("copilot-error", icon)
|
||||||
|
.on_click(cx.listener(move |this, _, cx| {
|
||||||
|
if let Some(workspace) = cx.window_handle().downcast::<Workspace>() {
|
||||||
|
workspace.update(cx, |workspace, cx| {
|
||||||
|
workspace.show_toast(
|
||||||
|
Toast::new(
|
||||||
|
COPILOT_ERROR_TOAST_ID,
|
||||||
|
format!("Copilot can't be started: {}", e),
|
||||||
|
)
|
||||||
|
.on_click(
|
||||||
|
"Reinstall Copilot",
|
||||||
|
|cx| {
|
||||||
|
if let Some(copilot) = Copilot::global(cx) {
|
||||||
|
copilot
|
||||||
|
.update(cx, |copilot, cx| copilot.reinstall(cx))
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.tooltip(|cx| Tooltip::text("GitHub Copilot", cx)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let this = cx.view().clone();
|
||||||
|
|
||||||
|
div().child(
|
||||||
|
popover_menu("copilot")
|
||||||
|
.menu(move |cx| match status {
|
||||||
|
Status::Authorized => this.update(cx, |this, cx| this.build_copilot_menu(cx)),
|
||||||
|
_ => this.update(cx, |this, cx| this.build_copilot_start_menu(cx)),
|
||||||
|
})
|
||||||
|
.anchor(AnchorCorner::BottomRight)
|
||||||
|
.trigger(
|
||||||
|
IconButton::new("copilot-icon", icon)
|
||||||
|
.tooltip(|cx| Tooltip::text("GitHub Copilot", cx)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CopilotButton {
|
||||||
|
pub fn new(fs: Arc<dyn Fs>, cx: &mut ViewContext<Self>) -> Self {
|
||||||
|
Copilot::global(cx).map(|copilot| cx.observe(&copilot, |_, _, cx| cx.notify()).detach());
|
||||||
|
|
||||||
|
cx.observe_global::<SettingsStore>(move |_, cx| cx.notify())
|
||||||
|
.detach();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
editor_subscription: None,
|
||||||
|
editor_enabled: None,
|
||||||
|
language: None,
|
||||||
|
file: None,
|
||||||
|
fs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_copilot_start_menu(&mut self, cx: &mut ViewContext<Self>) -> View<ContextMenu> {
|
||||||
|
let fs = self.fs.clone();
|
||||||
|
ContextMenu::build(cx, |menu, cx| {
|
||||||
|
menu.entry("Sign In", initiate_sign_in)
|
||||||
|
.entry("Disable Copilot", move |cx| hide_copilot(fs.clone(), cx))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_copilot_menu(&mut self, cx: &mut ViewContext<Self>) -> View<ContextMenu> {
|
||||||
|
let fs = self.fs.clone();
|
||||||
|
|
||||||
|
return ContextMenu::build(cx, move |mut menu, cx| {
|
||||||
|
if let Some(language) = self.language.clone() {
|
||||||
|
let fs = fs.clone();
|
||||||
|
let language_enabled =
|
||||||
|
language_settings::language_settings(Some(&language), None, cx)
|
||||||
|
.show_copilot_suggestions;
|
||||||
|
|
||||||
|
menu = menu.entry(
|
||||||
|
format!(
|
||||||
|
"{} Suggestions for {}",
|
||||||
|
if language_enabled { "Hide" } else { "Show" },
|
||||||
|
language.name()
|
||||||
|
),
|
||||||
|
move |cx| toggle_copilot_for_language(language.clone(), fs.clone(), cx),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let settings = AllLanguageSettings::get_global(cx);
|
||||||
|
|
||||||
|
if let Some(file) = &self.file {
|
||||||
|
let path = file.path().clone();
|
||||||
|
let path_enabled = settings.copilot_enabled_for_path(&path);
|
||||||
|
|
||||||
|
menu = menu.entry(
|
||||||
|
format!(
|
||||||
|
"{} Suggestions for This Path",
|
||||||
|
if path_enabled { "Hide" } else { "Show" }
|
||||||
|
),
|
||||||
|
move |cx| {
|
||||||
|
if let Some(workspace) = cx.window_handle().downcast::<Workspace>() {
|
||||||
|
if let Ok(workspace) = workspace.root_view(cx) {
|
||||||
|
let workspace = workspace.downgrade();
|
||||||
|
cx.spawn(|cx| {
|
||||||
|
configure_disabled_globs(
|
||||||
|
workspace,
|
||||||
|
path_enabled.then_some(path.clone()),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.detach_and_log_err(cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let globally_enabled = settings.copilot_enabled(None, None);
|
||||||
|
menu.entry(
|
||||||
|
if globally_enabled {
|
||||||
|
"Hide Suggestions for All Files"
|
||||||
|
} else {
|
||||||
|
"Show Suggestions for All Files"
|
||||||
|
},
|
||||||
|
move |cx| toggle_copilot_globally(fs.clone(), cx),
|
||||||
|
)
|
||||||
|
.separator()
|
||||||
|
.link(
|
||||||
|
"Copilot Settings",
|
||||||
|
OpenBrowser {
|
||||||
|
url: COPILOT_SETTINGS_URL.to_string(),
|
||||||
|
}
|
||||||
|
.boxed_clone(),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.action("Sign Out", SignOut.boxed_clone(), cx)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_enabled(&mut self, editor: View<Editor>, cx: &mut ViewContext<Self>) {
|
||||||
|
let editor = editor.read(cx);
|
||||||
|
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||||
|
let suggestion_anchor = editor.selections.newest_anchor().start;
|
||||||
|
let language = snapshot.language_at(suggestion_anchor);
|
||||||
|
let file = snapshot.file_at(suggestion_anchor).cloned();
|
||||||
|
|
||||||
|
self.editor_enabled = Some(
|
||||||
|
all_language_settings(self.file.as_ref(), cx)
|
||||||
|
.copilot_enabled(language, file.as_ref().map(|file| file.path().as_ref())),
|
||||||
|
);
|
||||||
|
self.language = language.cloned();
|
||||||
|
self.file = file;
|
||||||
|
|
||||||
|
cx.notify()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StatusItemView for CopilotButton {
|
||||||
|
fn set_active_pane_item(&mut self, item: Option<&dyn ItemHandle>, cx: &mut ViewContext<Self>) {
|
||||||
|
if let Some(editor) = item.map(|item| item.act_as::<Editor>(cx)).flatten() {
|
||||||
|
self.editor_subscription = Some((
|
||||||
|
cx.observe(&editor, Self::update_enabled),
|
||||||
|
editor.entity_id().as_u64() as usize,
|
||||||
|
));
|
||||||
|
self.update_enabled(editor, cx);
|
||||||
|
} else {
|
||||||
|
self.language = None;
|
||||||
|
self.editor_subscription = None;
|
||||||
|
self.editor_enabled = None;
|
||||||
|
}
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn configure_disabled_globs(
|
||||||
|
workspace: WeakView<Workspace>,
|
||||||
|
path_to_disable: Option<Arc<Path>>,
|
||||||
|
mut cx: AsyncWindowContext,
|
||||||
|
) -> Result<()> {
|
||||||
|
let settings_editor = workspace
|
||||||
|
.update(&mut cx, |_, cx| {
|
||||||
|
create_and_open_local_file(&paths::SETTINGS, cx, || {
|
||||||
|
settings::initial_user_settings_content().as_ref().into()
|
||||||
|
})
|
||||||
|
})?
|
||||||
|
.await?
|
||||||
|
.downcast::<Editor>()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
settings_editor.downgrade().update(&mut cx, |item, cx| {
|
||||||
|
let text = item.buffer().read(cx).snapshot(cx).text();
|
||||||
|
|
||||||
|
let settings = cx.global::<SettingsStore>();
|
||||||
|
let edits = settings.edits_for_update::<AllLanguageSettings>(&text, |file| {
|
||||||
|
let copilot = file.copilot.get_or_insert_with(Default::default);
|
||||||
|
let globs = copilot.disabled_globs.get_or_insert_with(|| {
|
||||||
|
settings
|
||||||
|
.get::<AllLanguageSettings>(None)
|
||||||
|
.copilot
|
||||||
|
.disabled_globs
|
||||||
|
.iter()
|
||||||
|
.map(|glob| glob.glob().to_string())
|
||||||
|
.collect()
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(path_to_disable) = &path_to_disable {
|
||||||
|
globs.push(path_to_disable.to_string_lossy().into_owned());
|
||||||
|
} else {
|
||||||
|
globs.clear();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if !edits.is_empty() {
|
||||||
|
item.change_selections(Some(Autoscroll::newest()), cx, |selections| {
|
||||||
|
selections.select_ranges(edits.iter().map(|e| e.0.clone()));
|
||||||
|
});
|
||||||
|
|
||||||
|
// When *enabling* a path, don't actually perform an edit, just select the range.
|
||||||
|
if path_to_disable.is_some() {
|
||||||
|
item.edit(edits.iter().cloned(), cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
anyhow::Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn toggle_copilot_globally(fs: Arc<dyn Fs>, cx: &mut AppContext) {
|
||||||
|
let show_copilot_suggestions = all_language_settings(None, cx).copilot_enabled(None, None);
|
||||||
|
update_settings_file::<AllLanguageSettings>(fs, cx, move |file| {
|
||||||
|
file.defaults.show_copilot_suggestions = Some((!show_copilot_suggestions).into())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn toggle_copilot_for_language(language: Arc<Language>, fs: Arc<dyn Fs>, cx: &mut AppContext) {
|
||||||
|
let show_copilot_suggestions =
|
||||||
|
all_language_settings(None, cx).copilot_enabled(Some(&language), None);
|
||||||
|
update_settings_file::<AllLanguageSettings>(fs, cx, move |file| {
|
||||||
|
file.languages
|
||||||
|
.entry(language.name())
|
||||||
|
.or_default()
|
||||||
|
.show_copilot_suggestions = Some(!show_copilot_suggestions);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hide_copilot(fs: Arc<dyn Fs>, cx: &mut AppContext) {
|
||||||
|
update_settings_file::<AllLanguageSettings>(fs, cx, move |file| {
|
||||||
|
file.features.get_or_insert(Default::default()).copilot = Some(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn initiate_sign_in(cx: &mut WindowContext) {
|
||||||
|
let Some(copilot) = Copilot::global(cx) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let status = copilot.read(cx).status();
|
||||||
|
|
||||||
|
match status {
|
||||||
|
Status::Starting { task } => {
|
||||||
|
let Some(workspace) = cx.window_handle().downcast::<Workspace>() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Ok(workspace) = workspace.update(cx, |workspace, cx| {
|
||||||
|
workspace.show_toast(
|
||||||
|
Toast::new(COPILOT_STARTING_TOAST_ID, "Copilot is starting..."),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
workspace.weak_handle()
|
||||||
|
}) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
cx.spawn(|mut cx| async move {
|
||||||
|
task.await;
|
||||||
|
if let Some(copilot) = cx.update(|_, cx| Copilot::global(cx)).ok().flatten() {
|
||||||
|
workspace
|
||||||
|
.update(&mut cx, |workspace, cx| match copilot.read(cx).status() {
|
||||||
|
Status::Authorized => workspace.show_toast(
|
||||||
|
Toast::new(COPILOT_STARTING_TOAST_ID, "Copilot has started!"),
|
||||||
|
cx,
|
||||||
|
),
|
||||||
|
_ => {
|
||||||
|
workspace.dismiss_toast(COPILOT_STARTING_TOAST_ID, cx);
|
||||||
|
copilot
|
||||||
|
.update(cx, |copilot, cx| copilot.sign_in(cx))
|
||||||
|
.detach_and_log_err(cx);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.log_err();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
copilot
|
||||||
|
.update(cx, |copilot, cx| copilot.sign_in(cx))
|
||||||
|
.detach_and_log_err(cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -774,24 +774,39 @@ fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
|
||||||
Arc::new(move |_| {
|
Arc::new(move |_| {
|
||||||
h_stack()
|
h_stack()
|
||||||
.id("diagnostic header")
|
.id("diagnostic header")
|
||||||
|
.py_2()
|
||||||
|
.pl_10()
|
||||||
|
.pr_5()
|
||||||
|
.w_full()
|
||||||
|
.justify_between()
|
||||||
|
.gap_2()
|
||||||
|
.child(
|
||||||
|
h_stack()
|
||||||
.gap_3()
|
.gap_3()
|
||||||
.bg(gpui::red())
|
|
||||||
.map(|stack| {
|
.map(|stack| {
|
||||||
let icon = if diagnostic.severity == DiagnosticSeverity::ERROR {
|
let icon = if diagnostic.severity == DiagnosticSeverity::ERROR {
|
||||||
IconElement::new(Icon::XCircle).color(Color::Error)
|
IconElement::new(Icon::XCircle).color(Color::Error)
|
||||||
} else {
|
} else {
|
||||||
IconElement::new(Icon::ExclamationTriangle).color(Color::Warning)
|
IconElement::new(Icon::ExclamationTriangle).color(Color::Warning)
|
||||||
};
|
};
|
||||||
|
stack.child(icon)
|
||||||
stack.child(div().pl_8().child(icon))
|
|
||||||
})
|
|
||||||
.when_some(diagnostic.source.as_ref(), |stack, source| {
|
|
||||||
stack.child(Label::new(format!("{source}:")).color(Color::Accent))
|
|
||||||
})
|
})
|
||||||
|
.child(
|
||||||
|
h_stack()
|
||||||
|
.gap_1()
|
||||||
.child(HighlightedLabel::new(message.clone(), highlights.clone()))
|
.child(HighlightedLabel::new(message.clone(), highlights.clone()))
|
||||||
.when_some(diagnostic.code.as_ref(), |stack, code| {
|
.when_some(diagnostic.code.as_ref(), |stack, code| {
|
||||||
stack.child(Label::new(code.clone()))
|
stack.child(Label::new(format!("({code})")).color(Color::Muted))
|
||||||
})
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
h_stack()
|
||||||
|
.gap_1()
|
||||||
|
.when_some(diagnostic.source.as_ref(), |stack, source| {
|
||||||
|
stack.child(Label::new(format!("{source}")).color(Color::Muted))
|
||||||
|
}),
|
||||||
|
)
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -802,11 +817,22 @@ pub(crate) fn render_summary(summary: &DiagnosticSummary) -> AnyElement {
|
||||||
label.into_any_element()
|
label.into_any_element()
|
||||||
} else {
|
} else {
|
||||||
h_stack()
|
h_stack()
|
||||||
.bg(gpui::red())
|
.gap_1()
|
||||||
.child(IconElement::new(Icon::XCircle))
|
.when(summary.error_count > 0, |then| {
|
||||||
.child(Label::new(summary.error_count.to_string()))
|
then.child(
|
||||||
.child(IconElement::new(Icon::ExclamationTriangle))
|
h_stack()
|
||||||
.child(Label::new(summary.warning_count.to_string()))
|
.gap_1()
|
||||||
|
.child(IconElement::new(Icon::XCircle).color(Color::Error))
|
||||||
|
.child(Label::new(summary.error_count.to_string())),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.when(summary.warning_count > 0, |then| {
|
||||||
|
then.child(
|
||||||
|
h_stack()
|
||||||
|
.child(IconElement::new(Icon::ExclamationTriangle).color(Color::Warning))
|
||||||
|
.child(Label::new(summary.warning_count.to_string())),
|
||||||
|
)
|
||||||
|
})
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -100,8 +100,10 @@ use text::{OffsetUtf16, Rope};
|
||||||
use theme::{
|
use theme::{
|
||||||
ActiveTheme, DiagnosticStyle, PlayerColor, SyntaxTheme, Theme, ThemeColors, ThemeSettings,
|
ActiveTheme, DiagnosticStyle, PlayerColor, SyntaxTheme, Theme, ThemeColors, ThemeSettings,
|
||||||
};
|
};
|
||||||
use ui::prelude::*;
|
use ui::{
|
||||||
use ui::{h_stack, v_stack, HighlightedLabel, IconButton, Popover, Tooltip};
|
h_stack, v_stack, ButtonSize, ButtonStyle, HighlightedLabel, Icon, IconButton, Popover, Tooltip,
|
||||||
|
};
|
||||||
|
use ui::{prelude::*, IconSize};
|
||||||
use util::{post_inc, RangeExt, ResultExt, TryFutureExt};
|
use util::{post_inc, RangeExt, ResultExt, TryFutureExt};
|
||||||
use workspace::{
|
use workspace::{
|
||||||
item::{ItemEvent, ItemHandle},
|
item::{ItemEvent, ItemHandle},
|
||||||
|
@ -154,7 +156,6 @@ pub fn render_parsed_markdown(
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
let runs = text_runs_for_highlights(&parsed.text, &editor_style.text, highlights);
|
|
||||||
|
|
||||||
let mut links = Vec::new();
|
let mut links = Vec::new();
|
||||||
let mut link_ranges = Vec::new();
|
let mut link_ranges = Vec::new();
|
||||||
|
@ -167,7 +168,7 @@ pub fn render_parsed_markdown(
|
||||||
|
|
||||||
InteractiveText::new(
|
InteractiveText::new(
|
||||||
element_id,
|
element_id,
|
||||||
StyledText::new(parsed.text.clone()).with_runs(runs),
|
StyledText::new(parsed.text.clone()).with_highlights(&editor_style.text, highlights),
|
||||||
)
|
)
|
||||||
.on_click(link_ranges, move |clicked_range_ix, cx| {
|
.on_click(link_ranges, move |clicked_range_ix, cx| {
|
||||||
match &links[clicked_range_ix] {
|
match &links[clicked_range_ix] {
|
||||||
|
@ -1199,11 +1200,7 @@ impl CompletionsMenu {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
let completion_label = StyledText::new(completion.label.text.clone())
|
let completion_label = StyledText::new(completion.label.text.clone())
|
||||||
.with_runs(text_runs_for_highlights(
|
.with_highlights(&style.text, highlights);
|
||||||
&completion.label.text,
|
|
||||||
&style.text,
|
|
||||||
highlights,
|
|
||||||
));
|
|
||||||
let documentation_label =
|
let documentation_label =
|
||||||
if let Some(Documentation::SingleLine(text)) = documentation {
|
if let Some(Documentation::SingleLine(text)) = documentation {
|
||||||
Some(SharedString::from(text.clone()))
|
Some(SharedString::from(text.clone()))
|
||||||
|
@ -1925,14 +1922,14 @@ impl Editor {
|
||||||
// self.buffer.read(cx).read(cx).file_at(point).cloned()
|
// self.buffer.read(cx).read(cx).file_at(point).cloned()
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// pub fn active_excerpt(
|
pub fn active_excerpt(
|
||||||
// &self,
|
&self,
|
||||||
// cx: &AppContext,
|
cx: &AppContext,
|
||||||
// ) -> Option<(ExcerptId, Model<Buffer>, Range<text::Anchor>)> {
|
) -> Option<(ExcerptId, Model<Buffer>, Range<text::Anchor>)> {
|
||||||
// self.buffer
|
self.buffer
|
||||||
// .read(cx)
|
.read(cx)
|
||||||
// .excerpt_containing(self.selections.newest_anchor().head(), cx)
|
.excerpt_containing(self.selections.newest_anchor().head(), cx)
|
||||||
// }
|
}
|
||||||
|
|
||||||
// pub fn style(&self, cx: &AppContext) -> EditorStyle {
|
// pub fn style(&self, cx: &AppContext) -> EditorStyle {
|
||||||
// build_style(
|
// build_style(
|
||||||
|
@ -9699,20 +9696,42 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> Rend
|
||||||
let message = diagnostic.message;
|
let message = diagnostic.message;
|
||||||
Arc::new(move |cx: &mut BlockContext| {
|
Arc::new(move |cx: &mut BlockContext| {
|
||||||
let message = message.clone();
|
let message = message.clone();
|
||||||
|
let copy_id: SharedString = format!("copy-{}", cx.block_id.clone()).to_string().into();
|
||||||
|
let write_to_clipboard = cx.write_to_clipboard(ClipboardItem::new(message.clone()));
|
||||||
|
|
||||||
|
// TODO: Nate: We should tint the background of the block with the severity color
|
||||||
|
// We need to extend the theme before we can do this
|
||||||
v_stack()
|
v_stack()
|
||||||
.id(cx.block_id)
|
.id(cx.block_id)
|
||||||
|
.relative()
|
||||||
.size_full()
|
.size_full()
|
||||||
.bg(gpui::red())
|
.bg(gpui::red())
|
||||||
.children(highlighted_lines.iter().map(|(line, highlights)| {
|
.children(highlighted_lines.iter().map(|(line, highlights)| {
|
||||||
div()
|
let group_id = cx.block_id.to_string();
|
||||||
|
|
||||||
|
h_stack()
|
||||||
|
.group(group_id.clone())
|
||||||
|
.gap_2()
|
||||||
|
.absolute()
|
||||||
|
.left(cx.anchor_x)
|
||||||
|
.px_1p5()
|
||||||
.child(HighlightedLabel::new(line.clone(), highlights.clone()))
|
.child(HighlightedLabel::new(line.clone(), highlights.clone()))
|
||||||
.ml(cx.anchor_x)
|
.child(
|
||||||
|
div()
|
||||||
|
.border()
|
||||||
|
.border_color(gpui::red())
|
||||||
|
.invisible()
|
||||||
|
.group_hover(group_id, |style| style.visible())
|
||||||
|
.child(
|
||||||
|
IconButton::new(copy_id.clone(), Icon::Copy)
|
||||||
|
.icon_color(Color::Muted)
|
||||||
|
.size(ButtonSize::Compact)
|
||||||
|
.style(ButtonStyle::Transparent)
|
||||||
|
.on_click(cx.listener(move |_, _, cx| write_to_clipboard))
|
||||||
|
.tooltip(|cx| Tooltip::text("Copy diagnostic message", cx)),
|
||||||
|
),
|
||||||
|
)
|
||||||
}))
|
}))
|
||||||
.cursor_pointer()
|
|
||||||
.on_click(cx.listener(move |_, _, cx| {
|
|
||||||
cx.write_to_clipboard(ClipboardItem::new(message.clone()));
|
|
||||||
}))
|
|
||||||
.tooltip(|cx| Tooltip::text("Copy diagnostic message", cx))
|
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -9760,31 +9779,6 @@ pub fn diagnostic_style(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn text_runs_for_highlights(
|
|
||||||
text: &str,
|
|
||||||
default_style: &TextStyle,
|
|
||||||
highlights: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
|
|
||||||
) -> Vec<TextRun> {
|
|
||||||
let mut runs = Vec::new();
|
|
||||||
let mut ix = 0;
|
|
||||||
for (range, highlight) in highlights {
|
|
||||||
if ix < range.start {
|
|
||||||
runs.push(default_style.clone().to_run(range.start - ix));
|
|
||||||
}
|
|
||||||
runs.push(
|
|
||||||
default_style
|
|
||||||
.clone()
|
|
||||||
.highlight(highlight)
|
|
||||||
.to_run(range.len()),
|
|
||||||
);
|
|
||||||
ix = range.end;
|
|
||||||
}
|
|
||||||
if ix < text.len() {
|
|
||||||
runs.push(default_style.to_run(text.len() - ix));
|
|
||||||
}
|
|
||||||
runs
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn styled_runs_for_code_label<'a>(
|
pub fn styled_runs_for_code_label<'a>(
|
||||||
label: &'a CodeLabel,
|
label: &'a CodeLabel,
|
||||||
syntax_theme: &'a theme::SyntaxTheme,
|
syntax_theme: &'a theme::SyntaxTheme,
|
||||||
|
|
|
@ -51,8 +51,10 @@ use std::{
|
||||||
};
|
};
|
||||||
use sum_tree::Bias;
|
use sum_tree::Bias;
|
||||||
use theme::{ActiveTheme, PlayerColor};
|
use theme::{ActiveTheme, PlayerColor};
|
||||||
use ui::prelude::*;
|
use ui::{
|
||||||
use ui::{h_stack, IconButton, Tooltip};
|
h_stack, ButtonLike, ButtonStyle, Disclosure, IconButton, IconElement, IconSize, Label, Tooltip,
|
||||||
|
};
|
||||||
|
use ui::{prelude::*, Icon};
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
use workspace::item::Item;
|
use workspace::item::Item;
|
||||||
|
|
||||||
|
@ -2223,7 +2225,8 @@ impl EditorElement {
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|project| project.read(cx).visible_worktrees(cx).count() > 1)
|
.map(|project| project.read(cx).visible_worktrees(cx).count() > 1)
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let jump_icon = project::File::from_dyn(buffer.file()).map(|file| {
|
|
||||||
|
let jump_handler = project::File::from_dyn(buffer.file()).map(|file| {
|
||||||
let jump_path = ProjectPath {
|
let jump_path = ProjectPath {
|
||||||
worktree_id: file.worktree_id(cx),
|
worktree_id: file.worktree_id(cx),
|
||||||
path: file.path.clone(),
|
path: file.path.clone(),
|
||||||
|
@ -2234,11 +2237,11 @@ impl EditorElement {
|
||||||
.map_or(range.context.start, |primary| primary.start);
|
.map_or(range.context.start, |primary| primary.start);
|
||||||
let jump_position = language::ToPoint::to_point(&jump_anchor, buffer);
|
let jump_position = language::ToPoint::to_point(&jump_anchor, buffer);
|
||||||
|
|
||||||
IconButton::new(block_id, ui::Icon::ArrowUpRight)
|
let jump_handler = cx.listener_for(&self.editor, move |editor, e, cx| {
|
||||||
.on_click(cx.listener_for(&self.editor, move |editor, e, cx| {
|
|
||||||
editor.jump(jump_path.clone(), jump_position, jump_anchor, cx);
|
editor.jump(jump_path.clone(), jump_position, jump_anchor, cx);
|
||||||
}))
|
});
|
||||||
.tooltip(|cx| Tooltip::for_action("Jump to Buffer", &OpenExcerpts, cx))
|
|
||||||
|
jump_handler
|
||||||
});
|
});
|
||||||
|
|
||||||
let element = if *starts_new_buffer {
|
let element = if *starts_new_buffer {
|
||||||
|
@ -2253,25 +2256,108 @@ impl EditorElement {
|
||||||
.map(|p| SharedString::from(p.to_string_lossy().to_string() + "/"));
|
.map(|p| SharedString::from(p.to_string_lossy().to_string() + "/"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let is_open = true;
|
||||||
|
|
||||||
|
div().id("path header container").size_full().p_1p5().child(
|
||||||
h_stack()
|
h_stack()
|
||||||
.id("path header block")
|
.id("path header block")
|
||||||
.size_full()
|
.py_1p5()
|
||||||
.bg(gpui::red())
|
.pl_3()
|
||||||
|
.pr_2()
|
||||||
|
.rounded_lg()
|
||||||
|
.shadow_md()
|
||||||
|
.border()
|
||||||
|
.border_color(cx.theme().colors().border)
|
||||||
|
.bg(cx.theme().colors().editor_subheader_background)
|
||||||
|
.justify_between()
|
||||||
|
.cursor_pointer()
|
||||||
|
.hover(|style| style.bg(cx.theme().colors().element_hover))
|
||||||
|
.on_click(cx.listener(|_editor, _event, _cx| {
|
||||||
|
// TODO: Implement collapsing path headers
|
||||||
|
todo!("Clicking path header")
|
||||||
|
}))
|
||||||
.child(
|
.child(
|
||||||
|
h_stack()
|
||||||
|
.gap_3()
|
||||||
|
// TODO: Add open/close state and toggle action
|
||||||
|
.child(
|
||||||
|
div().border().border_color(gpui::red()).child(
|
||||||
|
ButtonLike::new("path-header-disclosure-control")
|
||||||
|
.style(ButtonStyle::Subtle)
|
||||||
|
.child(IconElement::new(match is_open {
|
||||||
|
true => Icon::ChevronDown,
|
||||||
|
false => Icon::ChevronRight,
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
h_stack()
|
||||||
|
.gap_2()
|
||||||
|
.child(Label::new(
|
||||||
filename
|
filename
|
||||||
.map(SharedString::from)
|
.map(SharedString::from)
|
||||||
.unwrap_or_else(|| "untitled".into()),
|
.unwrap_or_else(|| "untitled".into()),
|
||||||
|
))
|
||||||
|
.when_some(parent_path, |then, path| {
|
||||||
|
then.child(Label::new(path).color(Color::Muted))
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.children(jump_handler.map(|jump_handler| {
|
||||||
|
IconButton::new(block_id, Icon::ArrowUpRight)
|
||||||
|
.style(ButtonStyle::Subtle)
|
||||||
|
.on_click(jump_handler)
|
||||||
|
.tooltip(|cx| {
|
||||||
|
Tooltip::for_action("Jump to Buffer", &OpenExcerpts, cx)
|
||||||
|
})
|
||||||
|
})), // .p_x(gutter_padding)
|
||||||
)
|
)
|
||||||
.children(parent_path)
|
|
||||||
.children(jump_icon) // .p_x(gutter_padding)
|
|
||||||
} else {
|
} else {
|
||||||
let text_style = style.text.clone();
|
let text_style = style.text.clone();
|
||||||
h_stack()
|
h_stack()
|
||||||
.id("collapsed context")
|
.id("collapsed context")
|
||||||
.size_full()
|
.size_full()
|
||||||
.bg(gpui::red())
|
.gap(gutter_padding)
|
||||||
.child("⋯")
|
.child(
|
||||||
.children(jump_icon) // .p_x(gutter_padding)
|
h_stack()
|
||||||
|
.justify_end()
|
||||||
|
.flex_none()
|
||||||
|
.w(gutter_width - gutter_padding)
|
||||||
|
.h_full()
|
||||||
|
.text_buffer(cx)
|
||||||
|
.text_color(cx.theme().colors().editor_line_number)
|
||||||
|
.child("..."),
|
||||||
|
)
|
||||||
|
.map(|this| {
|
||||||
|
if let Some(jump_handler) = jump_handler {
|
||||||
|
this.child(
|
||||||
|
ButtonLike::new("jump to collapsed context")
|
||||||
|
.style(ButtonStyle::Transparent)
|
||||||
|
.full_width()
|
||||||
|
.on_click(jump_handler)
|
||||||
|
.tooltip(|cx| {
|
||||||
|
Tooltip::for_action(
|
||||||
|
"Jump to Buffer",
|
||||||
|
&OpenExcerpts,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.h_px()
|
||||||
|
.w_full()
|
||||||
|
.bg(cx.theme().colors().border_variant)
|
||||||
|
.group_hover("", |style| {
|
||||||
|
style.bg(cx.theme().colors().border)
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
this.child(div().size_full().bg(gpui::green()))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// .child("⋯")
|
||||||
|
// .children(jump_icon) // .p_x(gutter_padding)
|
||||||
};
|
};
|
||||||
element.into_any()
|
element.into_any()
|
||||||
}
|
}
|
||||||
|
|
|
@ -992,10 +992,6 @@ impl Interactivity {
|
||||||
let interactive_bounds = interactive_bounds.clone();
|
let interactive_bounds = interactive_bounds.clone();
|
||||||
|
|
||||||
cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| {
|
cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| {
|
||||||
if phase != DispatchPhase::Bubble {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let is_hovered = interactive_bounds.visibly_contains(&event.position, cx)
|
let is_hovered = interactive_bounds.visibly_contains(&event.position, cx)
|
||||||
&& pending_mouse_down.borrow().is_none();
|
&& pending_mouse_down.borrow().is_none();
|
||||||
if !is_hovered {
|
if !is_hovered {
|
||||||
|
@ -1003,6 +999,10 @@ impl Interactivity {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if phase != DispatchPhase::Bubble {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if active_tooltip.borrow().is_none() {
|
if active_tooltip.borrow().is_none() {
|
||||||
let task = cx.spawn({
|
let task = cx.spawn({
|
||||||
let active_tooltip = active_tooltip.clone();
|
let active_tooltip = active_tooltip.clone();
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
Bounds, DispatchPhase, Element, ElementId, IntoElement, LayoutId, MouseDownEvent, MouseUpEvent,
|
Bounds, DispatchPhase, Element, ElementId, HighlightStyle, IntoElement, LayoutId,
|
||||||
Pixels, Point, SharedString, Size, TextRun, WhiteSpace, WindowContext, WrappedLine,
|
MouseDownEvent, MouseUpEvent, Pixels, Point, SharedString, Size, TextRun, TextStyle,
|
||||||
|
WhiteSpace, WindowContext, WrappedLine,
|
||||||
};
|
};
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use parking_lot::{Mutex, MutexGuard};
|
use parking_lot::{Mutex, MutexGuard};
|
||||||
|
@ -87,7 +88,28 @@ impl StyledText {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_runs(mut self, runs: Vec<TextRun>) -> Self {
|
pub fn with_highlights(
|
||||||
|
mut self,
|
||||||
|
default_style: &TextStyle,
|
||||||
|
highlights: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
|
||||||
|
) -> Self {
|
||||||
|
let mut runs = Vec::new();
|
||||||
|
let mut ix = 0;
|
||||||
|
for (range, highlight) in highlights {
|
||||||
|
if ix < range.start {
|
||||||
|
runs.push(default_style.clone().to_run(range.start - ix));
|
||||||
|
}
|
||||||
|
runs.push(
|
||||||
|
default_style
|
||||||
|
.clone()
|
||||||
|
.highlight(highlight)
|
||||||
|
.to_run(range.len()),
|
||||||
|
);
|
||||||
|
ix = range.end;
|
||||||
|
}
|
||||||
|
if ix < self.text.len() {
|
||||||
|
runs.push(default_style.to_run(self.text.len() - ix));
|
||||||
|
}
|
||||||
self.runs = Some(runs);
|
self.runs = Some(runs);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
|
@ -472,13 +472,27 @@ pub enum PromptLevel {
|
||||||
Critical,
|
Critical,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The style of the cursor (pointer)
|
||||||
#[derive(Copy, Clone, Debug)]
|
#[derive(Copy, Clone, Debug)]
|
||||||
pub enum CursorStyle {
|
pub enum CursorStyle {
|
||||||
Arrow,
|
Arrow,
|
||||||
ResizeLeftRight,
|
|
||||||
ResizeUpDown,
|
|
||||||
PointingHand,
|
|
||||||
IBeam,
|
IBeam,
|
||||||
|
Crosshair,
|
||||||
|
ClosedHand,
|
||||||
|
OpenHand,
|
||||||
|
PointingHand,
|
||||||
|
ResizeLeft,
|
||||||
|
ResizeRight,
|
||||||
|
ResizeLeftRight,
|
||||||
|
ResizeUp,
|
||||||
|
ResizeDown,
|
||||||
|
ResizeUpDown,
|
||||||
|
DisappearingItem,
|
||||||
|
IBeamCursorForVerticalLayout,
|
||||||
|
OperationNotAllowed,
|
||||||
|
DragLink,
|
||||||
|
DragCopy,
|
||||||
|
ContextualMenu,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for CursorStyle {
|
impl Default for CursorStyle {
|
||||||
|
|
|
@ -724,16 +724,35 @@ impl Platform for MacPlatform {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Match cursor style to one of the styles available
|
||||||
|
/// in macOS's [NSCursor](https://developer.apple.com/documentation/appkit/nscursor).
|
||||||
fn set_cursor_style(&self, style: CursorStyle) {
|
fn set_cursor_style(&self, style: CursorStyle) {
|
||||||
unsafe {
|
unsafe {
|
||||||
let new_cursor: id = match style {
|
let new_cursor: id = match style {
|
||||||
CursorStyle::Arrow => msg_send![class!(NSCursor), arrowCursor],
|
CursorStyle::Arrow => msg_send![class!(NSCursor), arrowCursor],
|
||||||
CursorStyle::ResizeLeftRight => {
|
|
||||||
msg_send![class!(NSCursor), resizeLeftRightCursor]
|
|
||||||
}
|
|
||||||
CursorStyle::ResizeUpDown => msg_send![class!(NSCursor), resizeUpDownCursor],
|
|
||||||
CursorStyle::PointingHand => msg_send![class!(NSCursor), pointingHandCursor],
|
|
||||||
CursorStyle::IBeam => msg_send![class!(NSCursor), IBeamCursor],
|
CursorStyle::IBeam => msg_send![class!(NSCursor), IBeamCursor],
|
||||||
|
CursorStyle::Crosshair => msg_send![class!(NSCursor), crosshairCursor],
|
||||||
|
CursorStyle::ClosedHand => msg_send![class!(NSCursor), closedHandCursor],
|
||||||
|
CursorStyle::OpenHand => msg_send![class!(NSCursor), openHandCursor],
|
||||||
|
CursorStyle::PointingHand => msg_send![class!(NSCursor), pointingHandCursor],
|
||||||
|
CursorStyle::ResizeLeft => msg_send![class!(NSCursor), resizeLeftCursor],
|
||||||
|
CursorStyle::ResizeRight => msg_send![class!(NSCursor), resizeRightCursor],
|
||||||
|
CursorStyle::ResizeLeftRight => msg_send![class!(NSCursor), resizeLeftRightCursor],
|
||||||
|
CursorStyle::ResizeUp => msg_send![class!(NSCursor), resizeUpCursor],
|
||||||
|
CursorStyle::ResizeDown => msg_send![class!(NSCursor), resizeDownCursor],
|
||||||
|
CursorStyle::ResizeUpDown => msg_send![class!(NSCursor), resizeUpDownCursor],
|
||||||
|
CursorStyle::DisappearingItem => {
|
||||||
|
msg_send![class!(NSCursor), disappearingItemCursor]
|
||||||
|
}
|
||||||
|
CursorStyle::IBeamCursorForVerticalLayout => {
|
||||||
|
msg_send![class!(NSCursor), IBeamCursorForVerticalLayout]
|
||||||
|
}
|
||||||
|
CursorStyle::OperationNotAllowed => {
|
||||||
|
msg_send![class!(NSCursor), operationNotAllowedCursor]
|
||||||
|
}
|
||||||
|
CursorStyle::DragLink => msg_send![class!(NSCursor), dragLinkCursor],
|
||||||
|
CursorStyle::DragCopy => msg_send![class!(NSCursor), dragCopyCursor],
|
||||||
|
CursorStyle::ContextualMenu => msg_send![class!(NSCursor), contextualMenuCursor],
|
||||||
};
|
};
|
||||||
|
|
||||||
let old_cursor: id = msg_send![class!(NSCursor), currentCursor];
|
let old_cursor: id = msg_send![class!(NSCursor), currentCursor];
|
||||||
|
|
|
@ -238,11 +238,11 @@ impl Platform for TestPlatform {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_to_clipboard(&self, item: crate::ClipboardItem) {
|
fn write_to_clipboard(&self, item: ClipboardItem) {
|
||||||
*self.current_clipboard_item.lock() = Some(item);
|
*self.current_clipboard_item.lock() = Some(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_from_clipboard(&self) -> Option<crate::ClipboardItem> {
|
fn read_from_clipboard(&self) -> Option<ClipboardItem> {
|
||||||
self.current_clipboard_item.lock().clone()
|
self.current_clipboard_item.lock().clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -101,6 +101,125 @@ pub trait Styled: Sized {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets cursor style when hovering over an element to `text`.
|
||||||
|
/// [Docs](https://tailwindcss.com/docs/cursor)
|
||||||
|
fn cursor_text(mut self) -> Self {
|
||||||
|
self.style().mouse_cursor = Some(CursorStyle::IBeam);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets cursor style when hovering over an element to `move`.
|
||||||
|
/// [Docs](https://tailwindcss.com/docs/cursor)
|
||||||
|
fn cursor_move(mut self) -> Self {
|
||||||
|
self.style().mouse_cursor = Some(CursorStyle::ClosedHand);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets cursor style when hovering over an element to `not-allowed`.
|
||||||
|
/// [Docs](https://tailwindcss.com/docs/cursor)
|
||||||
|
fn cursor_not_allowed(mut self) -> Self {
|
||||||
|
self.style().mouse_cursor = Some(CursorStyle::OperationNotAllowed);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets cursor style when hovering over an element to `context-menu`.
|
||||||
|
/// [Docs](https://tailwindcss.com/docs/cursor)
|
||||||
|
fn cursor_context_menu(mut self) -> Self {
|
||||||
|
self.style().mouse_cursor = Some(CursorStyle::ContextualMenu);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets cursor style when hovering over an element to `crosshair`.
|
||||||
|
/// [Docs](https://tailwindcss.com/docs/cursor)
|
||||||
|
fn cursor_crosshair(mut self) -> Self {
|
||||||
|
self.style().mouse_cursor = Some(CursorStyle::Crosshair);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets cursor style when hovering over an element to `vertical-text`.
|
||||||
|
/// [Docs](https://tailwindcss.com/docs/cursor)
|
||||||
|
fn cursor_vertical_text(mut self) -> Self {
|
||||||
|
self.style().mouse_cursor = Some(CursorStyle::IBeamCursorForVerticalLayout);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets cursor style when hovering over an element to `alias`.
|
||||||
|
/// [Docs](https://tailwindcss.com/docs/cursor)
|
||||||
|
fn cursor_alias(mut self) -> Self {
|
||||||
|
self.style().mouse_cursor = Some(CursorStyle::DragLink);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets cursor style when hovering over an element to `copy`.
|
||||||
|
/// [Docs](https://tailwindcss.com/docs/cursor)
|
||||||
|
fn cursor_copy(mut self) -> Self {
|
||||||
|
self.style().mouse_cursor = Some(CursorStyle::DragCopy);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets cursor style when hovering over an element to `no-drop`.
|
||||||
|
/// [Docs](https://tailwindcss.com/docs/cursor)
|
||||||
|
fn cursor_no_drop(mut self) -> Self {
|
||||||
|
self.style().mouse_cursor = Some(CursorStyle::OperationNotAllowed);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets cursor style when hovering over an element to `grab`.
|
||||||
|
/// [Docs](https://tailwindcss.com/docs/cursor)
|
||||||
|
fn cursor_grab(mut self) -> Self {
|
||||||
|
self.style().mouse_cursor = Some(CursorStyle::OpenHand);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets cursor style when hovering over an element to `grabbing`.
|
||||||
|
/// [Docs](https://tailwindcss.com/docs/cursor)
|
||||||
|
fn cursor_grabbing(mut self) -> Self {
|
||||||
|
self.style().mouse_cursor = Some(CursorStyle::ClosedHand);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets cursor style when hovering over an element to `col-resize`.
|
||||||
|
/// [Docs](https://tailwindcss.com/docs/cursor)
|
||||||
|
fn cursor_col_resize(mut self) -> Self {
|
||||||
|
self.style().mouse_cursor = Some(CursorStyle::ResizeLeftRight);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets cursor style when hovering over an element to `row-resize`.
|
||||||
|
/// [Docs](https://tailwindcss.com/docs/cursor)
|
||||||
|
fn cursor_row_resize(mut self) -> Self {
|
||||||
|
self.style().mouse_cursor = Some(CursorStyle::ResizeUpDown);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets cursor style when hovering over an element to `n-resize`.
|
||||||
|
/// [Docs](https://tailwindcss.com/docs/cursor)
|
||||||
|
fn cursor_n_resize(mut self) -> Self {
|
||||||
|
self.style().mouse_cursor = Some(CursorStyle::ResizeUp);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets cursor style when hovering over an element to `e-resize`.
|
||||||
|
/// [Docs](https://tailwindcss.com/docs/cursor)
|
||||||
|
fn cursor_e_resize(mut self) -> Self {
|
||||||
|
self.style().mouse_cursor = Some(CursorStyle::ResizeRight);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets cursor style when hovering over an element to `s-resize`.
|
||||||
|
/// [Docs](https://tailwindcss.com/docs/cursor)
|
||||||
|
fn cursor_s_resize(mut self) -> Self {
|
||||||
|
self.style().mouse_cursor = Some(CursorStyle::ResizeDown);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets cursor style when hovering over an element to `w-resize`.
|
||||||
|
/// [Docs](https://tailwindcss.com/docs/cursor)
|
||||||
|
fn cursor_w_resize(mut self) -> Self {
|
||||||
|
self.style().mouse_cursor = Some(CursorStyle::ResizeLeft);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Sets the whitespace of the element to `normal`.
|
/// Sets the whitespace of the element to `normal`.
|
||||||
/// [Docs](https://tailwindcss.com/docs/whitespace#normal)
|
/// [Docs](https://tailwindcss.com/docs/whitespace#normal)
|
||||||
fn whitespace_normal(mut self) -> Self {
|
fn whitespace_normal(mut self) -> Self {
|
||||||
|
|
26
crates/language_selector2/Cargo.toml
Normal file
26
crates/language_selector2/Cargo.toml
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
[package]
|
||||||
|
name = "language_selector2"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "src/language_selector.rs"
|
||||||
|
doctest = false
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
editor = { package = "editor2", path = "../editor2" }
|
||||||
|
fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
|
||||||
|
language = { package = "language2", path = "../language2" }
|
||||||
|
gpui = { package = "gpui2", path = "../gpui2" }
|
||||||
|
picker = { package = "picker2", path = "../picker2" }
|
||||||
|
project = { package = "project2", path = "../project2" }
|
||||||
|
theme = { package = "theme2", path = "../theme2" }
|
||||||
|
ui = { package = "ui2", path = "../ui2" }
|
||||||
|
settings = { package = "settings2", path = "../settings2" }
|
||||||
|
util = { path = "../util" }
|
||||||
|
workspace = { package = "workspace2", path = "../workspace2" }
|
||||||
|
anyhow.workspace = true
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
|
82
crates/language_selector2/src/active_buffer_language.rs
Normal file
82
crates/language_selector2/src/active_buffer_language.rs
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
use editor::Editor;
|
||||||
|
use gpui::{
|
||||||
|
div, Div, IntoElement, ParentElement, Render, Subscription, View, ViewContext, WeakView,
|
||||||
|
};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use ui::{Button, ButtonCommon, Clickable, Tooltip};
|
||||||
|
use workspace::{item::ItemHandle, StatusItemView, Workspace};
|
||||||
|
|
||||||
|
use crate::LanguageSelector;
|
||||||
|
|
||||||
|
pub struct ActiveBufferLanguage {
|
||||||
|
active_language: Option<Option<Arc<str>>>,
|
||||||
|
workspace: WeakView<Workspace>,
|
||||||
|
_observe_active_editor: Option<Subscription>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActiveBufferLanguage {
|
||||||
|
pub fn new(workspace: &Workspace) -> Self {
|
||||||
|
Self {
|
||||||
|
active_language: None,
|
||||||
|
workspace: workspace.weak_handle(),
|
||||||
|
_observe_active_editor: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_language(&mut self, editor: View<Editor>, cx: &mut ViewContext<Self>) {
|
||||||
|
self.active_language = Some(None);
|
||||||
|
|
||||||
|
let editor = editor.read(cx);
|
||||||
|
if let Some((_, buffer, _)) = editor.active_excerpt(cx) {
|
||||||
|
if let Some(language) = buffer.read(cx).language() {
|
||||||
|
self.active_language = Some(Some(language.name()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for ActiveBufferLanguage {
|
||||||
|
type Element = Div;
|
||||||
|
|
||||||
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> Div {
|
||||||
|
div().when_some(self.active_language.as_ref(), |el, active_language| {
|
||||||
|
let active_language_text = if let Some(active_language_text) = active_language {
|
||||||
|
active_language_text.to_string()
|
||||||
|
} else {
|
||||||
|
"Unknown".to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
el.child(
|
||||||
|
Button::new("change-language", active_language_text)
|
||||||
|
.on_click(cx.listener(|this, _, cx| {
|
||||||
|
if let Some(workspace) = this.workspace.upgrade() {
|
||||||
|
workspace.update(cx, |workspace, cx| {
|
||||||
|
LanguageSelector::toggle(workspace, cx)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.tooltip(|cx| Tooltip::text("Select Language", cx)),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StatusItemView for ActiveBufferLanguage {
|
||||||
|
fn set_active_pane_item(
|
||||||
|
&mut self,
|
||||||
|
active_pane_item: Option<&dyn ItemHandle>,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) {
|
||||||
|
if let Some(editor) = active_pane_item.and_then(|item| item.act_as::<Editor>(cx)) {
|
||||||
|
self._observe_active_editor = Some(cx.observe(&editor, Self::update_language));
|
||||||
|
self.update_language(editor, cx);
|
||||||
|
} else {
|
||||||
|
self.active_language = None;
|
||||||
|
self._observe_active_editor = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
231
crates/language_selector2/src/language_selector.rs
Normal file
231
crates/language_selector2/src/language_selector.rs
Normal file
|
@ -0,0 +1,231 @@
|
||||||
|
mod active_buffer_language;
|
||||||
|
|
||||||
|
pub use active_buffer_language::ActiveBufferLanguage;
|
||||||
|
use anyhow::anyhow;
|
||||||
|
use editor::Editor;
|
||||||
|
use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
|
||||||
|
use gpui::{
|
||||||
|
actions, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView, Model,
|
||||||
|
ParentElement, Render, Styled, View, ViewContext, VisualContext, WeakView,
|
||||||
|
};
|
||||||
|
use language::{Buffer, LanguageRegistry};
|
||||||
|
use picker::{Picker, PickerDelegate};
|
||||||
|
use project::Project;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use ui::{v_stack, HighlightedLabel, ListItem, Selectable};
|
||||||
|
use util::ResultExt;
|
||||||
|
use workspace::Workspace;
|
||||||
|
|
||||||
|
actions!(Toggle);
|
||||||
|
|
||||||
|
pub fn init(cx: &mut AppContext) {
|
||||||
|
cx.observe_new_views(LanguageSelector::register).detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct LanguageSelector {
|
||||||
|
picker: View<Picker<LanguageSelectorDelegate>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LanguageSelector {
|
||||||
|
fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
|
||||||
|
workspace.register_action(move |workspace, _: &Toggle, cx| {
|
||||||
|
Self::toggle(workspace, cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn toggle(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> Option<()> {
|
||||||
|
let registry = workspace.app_state().languages.clone();
|
||||||
|
let (_, buffer, _) = workspace
|
||||||
|
.active_item(cx)?
|
||||||
|
.act_as::<Editor>(cx)?
|
||||||
|
.read(cx)
|
||||||
|
.active_excerpt(cx)?;
|
||||||
|
let project = workspace.project().clone();
|
||||||
|
|
||||||
|
workspace.toggle_modal(cx, move |cx| {
|
||||||
|
LanguageSelector::new(buffer, project, registry, cx)
|
||||||
|
});
|
||||||
|
Some(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new(
|
||||||
|
buffer: Model<Buffer>,
|
||||||
|
project: Model<Project>,
|
||||||
|
language_registry: Arc<LanguageRegistry>,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) -> Self {
|
||||||
|
let delegate = LanguageSelectorDelegate::new(
|
||||||
|
cx.view().downgrade(),
|
||||||
|
buffer,
|
||||||
|
project,
|
||||||
|
language_registry,
|
||||||
|
);
|
||||||
|
|
||||||
|
let picker = cx.build_view(|cx| Picker::new(delegate, cx));
|
||||||
|
Self { picker }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for LanguageSelector {
|
||||||
|
type Element = Div;
|
||||||
|
|
||||||
|
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
|
||||||
|
v_stack().min_w_96().child(self.picker.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FocusableView for LanguageSelector {
|
||||||
|
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
||||||
|
self.picker.focus_handle(cx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl EventEmitter<DismissEvent> for LanguageSelector {}
|
||||||
|
|
||||||
|
pub struct LanguageSelectorDelegate {
|
||||||
|
language_selector: WeakView<LanguageSelector>,
|
||||||
|
buffer: Model<Buffer>,
|
||||||
|
project: Model<Project>,
|
||||||
|
language_registry: Arc<LanguageRegistry>,
|
||||||
|
candidates: Vec<StringMatchCandidate>,
|
||||||
|
matches: Vec<StringMatch>,
|
||||||
|
selected_index: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LanguageSelectorDelegate {
|
||||||
|
fn new(
|
||||||
|
language_selector: WeakView<LanguageSelector>,
|
||||||
|
buffer: Model<Buffer>,
|
||||||
|
project: Model<Project>,
|
||||||
|
language_registry: Arc<LanguageRegistry>,
|
||||||
|
) -> Self {
|
||||||
|
let candidates = language_registry
|
||||||
|
.language_names()
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(candidate_id, name)| StringMatchCandidate::new(candidate_id, name))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
language_selector,
|
||||||
|
buffer,
|
||||||
|
project,
|
||||||
|
language_registry,
|
||||||
|
candidates,
|
||||||
|
matches: vec![],
|
||||||
|
selected_index: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PickerDelegate for LanguageSelectorDelegate {
|
||||||
|
type ListItem = ListItem;
|
||||||
|
|
||||||
|
fn placeholder_text(&self) -> Arc<str> {
|
||||||
|
"Select a language...".into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn match_count(&self) -> usize {
|
||||||
|
self.matches.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
|
||||||
|
if let Some(mat) = self.matches.get(self.selected_index) {
|
||||||
|
let language_name = &self.candidates[mat.candidate_id].string;
|
||||||
|
let language = self.language_registry.language_for_name(language_name);
|
||||||
|
let project = self.project.downgrade();
|
||||||
|
let buffer = self.buffer.downgrade();
|
||||||
|
cx.spawn(|_, mut cx| async move {
|
||||||
|
let language = language.await?;
|
||||||
|
let project = project
|
||||||
|
.upgrade()
|
||||||
|
.ok_or_else(|| anyhow!("project was dropped"))?;
|
||||||
|
let buffer = buffer
|
||||||
|
.upgrade()
|
||||||
|
.ok_or_else(|| anyhow!("buffer was dropped"))?;
|
||||||
|
project.update(&mut cx, |project, cx| {
|
||||||
|
project.set_language_for_buffer(&buffer, language, cx);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.detach_and_log_err(cx);
|
||||||
|
}
|
||||||
|
self.dismissed(cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
|
||||||
|
self.language_selector
|
||||||
|
.update(cx, |_, cx| cx.emit(DismissEvent))
|
||||||
|
.log_err();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn selected_index(&self) -> usize {
|
||||||
|
self.selected_index
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Picker<Self>>) {
|
||||||
|
self.selected_index = ix;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_matches(
|
||||||
|
&mut self,
|
||||||
|
query: String,
|
||||||
|
cx: &mut ViewContext<Picker<Self>>,
|
||||||
|
) -> gpui::Task<()> {
|
||||||
|
let background = cx.background_executor().clone();
|
||||||
|
let candidates = self.candidates.clone();
|
||||||
|
cx.spawn(|this, mut cx| async move {
|
||||||
|
let matches = if query.is_empty() {
|
||||||
|
candidates
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(index, candidate)| StringMatch {
|
||||||
|
candidate_id: index,
|
||||||
|
string: candidate.string,
|
||||||
|
positions: Vec::new(),
|
||||||
|
score: 0.0,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
} else {
|
||||||
|
match_strings(
|
||||||
|
&candidates,
|
||||||
|
&query,
|
||||||
|
false,
|
||||||
|
100,
|
||||||
|
&Default::default(),
|
||||||
|
background,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
};
|
||||||
|
|
||||||
|
this.update(&mut cx, |this, cx| {
|
||||||
|
let delegate = &mut this.delegate;
|
||||||
|
delegate.matches = matches;
|
||||||
|
delegate.selected_index = delegate
|
||||||
|
.selected_index
|
||||||
|
.min(delegate.matches.len().saturating_sub(1));
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
.log_err();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_match(
|
||||||
|
&self,
|
||||||
|
ix: usize,
|
||||||
|
selected: bool,
|
||||||
|
cx: &mut ViewContext<Picker<Self>>,
|
||||||
|
) -> Option<Self::ListItem> {
|
||||||
|
let mat = &self.matches[ix];
|
||||||
|
let buffer_language_name = self.buffer.read(cx).language().map(|l| l.name());
|
||||||
|
let mut label = mat.string.clone();
|
||||||
|
if buffer_language_name.as_deref() == Some(mat.string.as_str()) {
|
||||||
|
label.push_str(" (current)");
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(
|
||||||
|
ListItem::new(ix)
|
||||||
|
.inset(true)
|
||||||
|
.selected(selected)
|
||||||
|
.child(HighlightedLabel::new(label, mat.positions.clone())),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -178,6 +178,15 @@ impl<D: PickerDelegate> Picker<D> {
|
||||||
}
|
}
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn query(&self, cx: &AppContext) -> String {
|
||||||
|
self.editor.read(cx).text(cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_query(&self, query: impl Into<Arc<str>>, cx: &mut ViewContext<Self>) {
|
||||||
|
self.editor
|
||||||
|
.update(cx, |editor, cx| editor.set_text(query, cx));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<D: PickerDelegate> Render for Picker<D> {
|
impl<D: PickerDelegate> Render for Picker<D> {
|
||||||
|
|
|
@ -1661,14 +1661,15 @@ impl Project {
|
||||||
path: impl Into<ProjectPath>,
|
path: impl Into<ProjectPath>,
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) -> Task<Result<(ProjectEntryId, AnyModelHandle)>> {
|
) -> Task<Result<(ProjectEntryId, AnyModelHandle)>> {
|
||||||
let task = self.open_buffer(path, cx);
|
let project_path = path.into();
|
||||||
|
let task = self.open_buffer(project_path.clone(), cx);
|
||||||
cx.spawn_weak(|_, cx| async move {
|
cx.spawn_weak(|_, cx| async move {
|
||||||
let buffer = task.await?;
|
let buffer = task.await?;
|
||||||
let project_entry_id = buffer
|
let project_entry_id = buffer
|
||||||
.read_with(&cx, |buffer, cx| {
|
.read_with(&cx, |buffer, cx| {
|
||||||
File::from_dyn(buffer.file()).and_then(|file| file.project_entry_id(cx))
|
File::from_dyn(buffer.file()).and_then(|file| file.project_entry_id(cx))
|
||||||
})
|
})
|
||||||
.ok_or_else(|| anyhow!("no project entry"))?;
|
.with_context(|| format!("no project entry for {project_path:?}"))?;
|
||||||
|
|
||||||
let buffer: &AnyModelHandle = &buffer;
|
let buffer: &AnyModelHandle = &buffer;
|
||||||
Ok((project_entry_id, buffer.clone()))
|
Ok((project_entry_id, buffer.clone()))
|
||||||
|
|
|
@ -1691,14 +1691,15 @@ impl Project {
|
||||||
path: impl Into<ProjectPath>,
|
path: impl Into<ProjectPath>,
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) -> Task<Result<(ProjectEntryId, AnyModel)>> {
|
) -> Task<Result<(ProjectEntryId, AnyModel)>> {
|
||||||
let task = self.open_buffer(path, cx);
|
let project_path = path.into();
|
||||||
|
let task = self.open_buffer(project_path.clone(), cx);
|
||||||
cx.spawn(move |_, mut cx| async move {
|
cx.spawn(move |_, mut cx| async move {
|
||||||
let buffer = task.await?;
|
let buffer = task.await?;
|
||||||
let project_entry_id = buffer
|
let project_entry_id = buffer
|
||||||
.update(&mut cx, |buffer, cx| {
|
.update(&mut cx, |buffer, cx| {
|
||||||
File::from_dyn(buffer.file()).and_then(|file| file.project_entry_id(cx))
|
File::from_dyn(buffer.file()).and_then(|file| file.project_entry_id(cx))
|
||||||
})?
|
})?
|
||||||
.ok_or_else(|| anyhow!("no project entry"))?;
|
.with_context(|| format!("no project entry for {project_path:?}"))?;
|
||||||
|
|
||||||
let buffer: &AnyModel = &buffer;
|
let buffer: &AnyModel = &buffer;
|
||||||
Ok((project_entry_id, buffer.clone()))
|
Ok((project_entry_id, buffer.clone()))
|
||||||
|
|
|
@ -9,4 +9,4 @@ pub use notification::*;
|
||||||
pub use peer::*;
|
pub use peer::*;
|
||||||
mod macros;
|
mod macros;
|
||||||
|
|
||||||
pub const PROTOCOL_VERSION: u32 = 64;
|
pub const PROTOCOL_VERSION: u32 = 66;
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
mod auto_height_editor;
|
mod auto_height_editor;
|
||||||
|
mod cursor;
|
||||||
mod focus;
|
mod focus;
|
||||||
mod kitchen_sink;
|
mod kitchen_sink;
|
||||||
mod picker;
|
mod picker;
|
||||||
|
@ -7,6 +8,7 @@ mod text;
|
||||||
mod z_index;
|
mod z_index;
|
||||||
|
|
||||||
pub use auto_height_editor::*;
|
pub use auto_height_editor::*;
|
||||||
|
pub use cursor::*;
|
||||||
pub use focus::*;
|
pub use focus::*;
|
||||||
pub use kitchen_sink::*;
|
pub use kitchen_sink::*;
|
||||||
pub use picker::*;
|
pub use picker::*;
|
||||||
|
|
112
crates/storybook2/src/stories/cursor.rs
Normal file
112
crates/storybook2/src/stories/cursor.rs
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
use gpui::{Div, Render, Stateful};
|
||||||
|
use story::Story;
|
||||||
|
use ui::prelude::*;
|
||||||
|
|
||||||
|
pub struct CursorStory;
|
||||||
|
|
||||||
|
impl Render for CursorStory {
|
||||||
|
type Element = Div;
|
||||||
|
|
||||||
|
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
|
||||||
|
let all_cursors: [(&str, Box<dyn Fn(Stateful<Div>) -> Stateful<Div>>); 19] = [
|
||||||
|
(
|
||||||
|
"cursor_default",
|
||||||
|
Box::new(|el: Stateful<Div>| el.cursor_default()),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"cursor_pointer",
|
||||||
|
Box::new(|el: Stateful<Div>| el.cursor_pointer()),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"cursor_text",
|
||||||
|
Box::new(|el: Stateful<Div>| el.cursor_text()),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"cursor_move",
|
||||||
|
Box::new(|el: Stateful<Div>| el.cursor_move()),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"cursor_not_allowed",
|
||||||
|
Box::new(|el: Stateful<Div>| el.cursor_not_allowed()),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"cursor_context_menu",
|
||||||
|
Box::new(|el: Stateful<Div>| el.cursor_context_menu()),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"cursor_crosshair",
|
||||||
|
Box::new(|el: Stateful<Div>| el.cursor_crosshair()),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"cursor_vertical_text",
|
||||||
|
Box::new(|el: Stateful<Div>| el.cursor_vertical_text()),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"cursor_alias",
|
||||||
|
Box::new(|el: Stateful<Div>| el.cursor_alias()),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"cursor_copy",
|
||||||
|
Box::new(|el: Stateful<Div>| el.cursor_copy()),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"cursor_no_drop",
|
||||||
|
Box::new(|el: Stateful<Div>| el.cursor_no_drop()),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"cursor_grab",
|
||||||
|
Box::new(|el: Stateful<Div>| el.cursor_grab()),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"cursor_grabbing",
|
||||||
|
Box::new(|el: Stateful<Div>| el.cursor_grabbing()),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"cursor_col_resize",
|
||||||
|
Box::new(|el: Stateful<Div>| el.cursor_col_resize()),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"cursor_row_resize",
|
||||||
|
Box::new(|el: Stateful<Div>| el.cursor_row_resize()),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"cursor_n_resize",
|
||||||
|
Box::new(|el: Stateful<Div>| el.cursor_n_resize()),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"cursor_e_resize",
|
||||||
|
Box::new(|el: Stateful<Div>| el.cursor_e_resize()),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"cursor_s_resize",
|
||||||
|
Box::new(|el: Stateful<Div>| el.cursor_s_resize()),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"cursor_w_resize",
|
||||||
|
Box::new(|el: Stateful<Div>| el.cursor_w_resize()),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
Story::container()
|
||||||
|
.flex()
|
||||||
|
.gap_1()
|
||||||
|
.child(Story::title("cursor"))
|
||||||
|
.children(all_cursors.map(|(name, apply_cursor)| {
|
||||||
|
div().gap_1().flex().text_color(gpui::white()).child(
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.justify_center()
|
||||||
|
.id(name)
|
||||||
|
.map(apply_cursor)
|
||||||
|
.w_64()
|
||||||
|
.h_8()
|
||||||
|
.bg(gpui::red())
|
||||||
|
.hover(|style| style.bg(gpui::blue()))
|
||||||
|
.active(|style| style.bg(gpui::green()))
|
||||||
|
.text_sm()
|
||||||
|
.child(Story::label(name)),
|
||||||
|
)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
use gpui::{
|
use gpui::{
|
||||||
blue, div, green, red, white, Div, InteractiveText, ParentElement, Render, Styled, StyledText,
|
blue, div, green, red, white, Div, HighlightStyle, InteractiveText, ParentElement, Render,
|
||||||
TextRun, View, VisualContext, WindowContext,
|
Styled, StyledText, View, VisualContext, WindowContext,
|
||||||
};
|
};
|
||||||
use ui::v_stack;
|
use ui::v_stack;
|
||||||
|
|
||||||
|
@ -59,13 +59,11 @@ impl Render for TextStory {
|
||||||
))).child(
|
))).child(
|
||||||
InteractiveText::new(
|
InteractiveText::new(
|
||||||
"interactive",
|
"interactive",
|
||||||
StyledText::new("Hello world, how is it going?").with_runs(vec![
|
StyledText::new("Hello world, how is it going?").with_highlights(&cx.text_style(), [
|
||||||
cx.text_style().to_run(6),
|
(6..11, HighlightStyle {
|
||||||
TextRun {
|
|
||||||
background_color: Some(green()),
|
background_color: Some(green()),
|
||||||
..cx.text_style().to_run(5)
|
..Default::default()
|
||||||
},
|
}),
|
||||||
cx.text_style().to_run(18),
|
|
||||||
]),
|
]),
|
||||||
)
|
)
|
||||||
.on_click(vec![2..4, 1..3, 7..9], |range_ix, _cx| {
|
.on_click(vec![2..4, 1..3, 7..9], |range_ix, _cx| {
|
||||||
|
|
|
@ -17,6 +17,7 @@ pub enum ComponentStory {
|
||||||
Button,
|
Button,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
ContextMenu,
|
ContextMenu,
|
||||||
|
Cursor,
|
||||||
Disclosure,
|
Disclosure,
|
||||||
Focus,
|
Focus,
|
||||||
Icon,
|
Icon,
|
||||||
|
@ -40,6 +41,7 @@ impl ComponentStory {
|
||||||
Self::Button => cx.build_view(|_| ui::ButtonStory).into(),
|
Self::Button => cx.build_view(|_| ui::ButtonStory).into(),
|
||||||
Self::Checkbox => cx.build_view(|_| ui::CheckboxStory).into(),
|
Self::Checkbox => cx.build_view(|_| ui::CheckboxStory).into(),
|
||||||
Self::ContextMenu => cx.build_view(|_| ui::ContextMenuStory).into(),
|
Self::ContextMenu => cx.build_view(|_| ui::ContextMenuStory).into(),
|
||||||
|
Self::Cursor => cx.build_view(|_| crate::stories::CursorStory).into(),
|
||||||
Self::Disclosure => cx.build_view(|_| ui::DisclosureStory).into(),
|
Self::Disclosure => cx.build_view(|_| ui::DisclosureStory).into(),
|
||||||
Self::Focus => FocusStory::view(cx).into(),
|
Self::Focus => FocusStory::view(cx).into(),
|
||||||
Self::Icon => cx.build_view(|_| ui::IconStory).into(),
|
Self::Icon => cx.build_view(|_| ui::IconStory).into(),
|
||||||
|
|
|
@ -52,13 +52,13 @@ pub(crate) fn one_dark() -> Theme {
|
||||||
element_hover: hsla(225.0 / 360., 11.8 / 100., 26.7 / 100., 1.0),
|
element_hover: hsla(225.0 / 360., 11.8 / 100., 26.7 / 100., 1.0),
|
||||||
element_active: hsla(220.0 / 360., 11.8 / 100., 20.0 / 100., 1.0),
|
element_active: hsla(220.0 / 360., 11.8 / 100., 20.0 / 100., 1.0),
|
||||||
element_selected: hsla(224.0 / 360., 11.3 / 100., 26.1 / 100., 1.0),
|
element_selected: hsla(224.0 / 360., 11.3 / 100., 26.1 / 100., 1.0),
|
||||||
element_disabled: hsla(224.0 / 360., 11.3 / 100., 26.1 / 100., 1.0),
|
element_disabled: SystemColors::default().transparent,
|
||||||
drop_target_background: hsla(220.0 / 360., 8.3 / 100., 21.4 / 100., 1.0),
|
drop_target_background: hsla(220.0 / 360., 8.3 / 100., 21.4 / 100., 1.0),
|
||||||
ghost_element_background: SystemColors::default().transparent,
|
ghost_element_background: SystemColors::default().transparent,
|
||||||
ghost_element_hover: hsla(225.0 / 360., 11.8 / 100., 26.7 / 100., 1.0),
|
ghost_element_hover: hsla(225.0 / 360., 11.8 / 100., 26.7 / 100., 1.0),
|
||||||
ghost_element_active: hsla(220.0 / 360., 11.8 / 100., 20.0 / 100., 1.0),
|
ghost_element_active: hsla(220.0 / 360., 11.8 / 100., 20.0 / 100., 1.0),
|
||||||
ghost_element_selected: hsla(224.0 / 360., 11.3 / 100., 26.1 / 100., 1.0),
|
ghost_element_selected: hsla(224.0 / 360., 11.3 / 100., 26.1 / 100., 1.0),
|
||||||
ghost_element_disabled: hsla(224.0 / 360., 11.3 / 100., 26.1 / 100., 1.0),
|
ghost_element_disabled: SystemColors::default().transparent,
|
||||||
text: hsla(221. / 360., 11. / 100., 86. / 100., 1.0),
|
text: hsla(221. / 360., 11. / 100., 86. / 100., 1.0),
|
||||||
text_muted: hsla(218.0 / 360., 7. / 100., 46. / 100., 1.0),
|
text_muted: hsla(218.0 / 360., 7. / 100., 46. / 100., 1.0),
|
||||||
text_placeholder: hsla(220.0 / 360., 6.6 / 100., 44.5 / 100., 1.0),
|
text_placeholder: hsla(220.0 / 360., 6.6 / 100., 44.5 / 100., 1.0),
|
||||||
|
|
|
@ -9,6 +9,8 @@ mod keybinding;
|
||||||
mod label;
|
mod label;
|
||||||
mod list;
|
mod list;
|
||||||
mod popover;
|
mod popover;
|
||||||
|
mod popover_menu;
|
||||||
|
mod right_click_menu;
|
||||||
mod stack;
|
mod stack;
|
||||||
mod tooltip;
|
mod tooltip;
|
||||||
|
|
||||||
|
@ -26,6 +28,8 @@ pub use keybinding::*;
|
||||||
pub use label::*;
|
pub use label::*;
|
||||||
pub use list::*;
|
pub use list::*;
|
||||||
pub use popover::*;
|
pub use popover::*;
|
||||||
|
pub use popover_menu::*;
|
||||||
|
pub use right_click_menu::*;
|
||||||
pub use stack::*;
|
pub use stack::*;
|
||||||
pub use tooltip::*;
|
pub use tooltip::*;
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
mod button;
|
mod button;
|
||||||
|
pub(self) mod button_icon;
|
||||||
mod button_like;
|
mod button_like;
|
||||||
mod icon_button;
|
mod icon_button;
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,22 @@
|
||||||
use gpui::AnyView;
|
use gpui::{AnyView, DefiniteLength};
|
||||||
|
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::{ButtonCommon, ButtonLike, ButtonSize, ButtonStyle, Label, LineHeightStyle};
|
use crate::{
|
||||||
|
ButtonCommon, ButtonLike, ButtonSize, ButtonStyle, Icon, IconSize, Label, LineHeightStyle,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::button_icon::ButtonIcon;
|
||||||
|
|
||||||
#[derive(IntoElement)]
|
#[derive(IntoElement)]
|
||||||
pub struct Button {
|
pub struct Button {
|
||||||
base: ButtonLike,
|
base: ButtonLike,
|
||||||
label: SharedString,
|
label: SharedString,
|
||||||
label_color: Option<Color>,
|
label_color: Option<Color>,
|
||||||
|
selected_label: Option<SharedString>,
|
||||||
|
icon: Option<Icon>,
|
||||||
|
icon_size: Option<IconSize>,
|
||||||
|
icon_color: Option<Color>,
|
||||||
|
selected_icon: Option<Icon>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Button {
|
impl Button {
|
||||||
|
@ -16,6 +25,11 @@ impl Button {
|
||||||
base: ButtonLike::new(id),
|
base: ButtonLike::new(id),
|
||||||
label: label.into(),
|
label: label.into(),
|
||||||
label_color: None,
|
label_color: None,
|
||||||
|
selected_label: None,
|
||||||
|
icon: None,
|
||||||
|
icon_size: None,
|
||||||
|
icon_color: None,
|
||||||
|
selected_icon: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,6 +37,31 @@ impl Button {
|
||||||
self.label_color = label_color.into();
|
self.label_color = label_color.into();
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn selected_label<L: Into<SharedString>>(mut self, label: impl Into<Option<L>>) -> Self {
|
||||||
|
self.selected_label = label.into().map(Into::into);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn icon(mut self, icon: impl Into<Option<Icon>>) -> Self {
|
||||||
|
self.icon = icon.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn icon_size(mut self, icon_size: impl Into<Option<IconSize>>) -> Self {
|
||||||
|
self.icon_size = icon_size.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn icon_color(mut self, icon_color: impl Into<Option<Color>>) -> Self {
|
||||||
|
self.icon_color = icon_color.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn selected_icon(mut self, icon: impl Into<Option<Icon>>) -> Self {
|
||||||
|
self.selected_icon = icon.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Selectable for Button {
|
impl Selectable for Button {
|
||||||
|
@ -49,6 +88,18 @@ impl Clickable for Button {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl FixedWidth for Button {
|
||||||
|
fn width(mut self, width: DefiniteLength) -> Self {
|
||||||
|
self.base = self.base.width(width);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn full_width(mut self) -> Self {
|
||||||
|
self.base = self.base.full_width();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl ButtonCommon for Button {
|
impl ButtonCommon for Button {
|
||||||
fn id(&self) -> &ElementId {
|
fn id(&self) -> &ElementId {
|
||||||
self.base.id()
|
self.base.id()
|
||||||
|
@ -74,16 +125,33 @@ impl RenderOnce for Button {
|
||||||
type Rendered = ButtonLike;
|
type Rendered = ButtonLike;
|
||||||
|
|
||||||
fn render(self, _cx: &mut WindowContext) -> Self::Rendered {
|
fn render(self, _cx: &mut WindowContext) -> Self::Rendered {
|
||||||
let label_color = if self.base.disabled {
|
let is_disabled = self.base.disabled;
|
||||||
|
let is_selected = self.base.selected;
|
||||||
|
|
||||||
|
let label = self
|
||||||
|
.selected_label
|
||||||
|
.filter(|_| is_selected)
|
||||||
|
.unwrap_or(self.label);
|
||||||
|
|
||||||
|
let label_color = if is_disabled {
|
||||||
Color::Disabled
|
Color::Disabled
|
||||||
} else if self.base.selected {
|
} else if is_selected {
|
||||||
Color::Selected
|
Color::Selected
|
||||||
} else {
|
} else {
|
||||||
self.label_color.unwrap_or_default()
|
self.label_color.unwrap_or_default()
|
||||||
};
|
};
|
||||||
|
|
||||||
self.base.child(
|
self.base
|
||||||
Label::new(self.label)
|
.children(self.icon.map(|icon| {
|
||||||
|
ButtonIcon::new(icon)
|
||||||
|
.disabled(is_disabled)
|
||||||
|
.selected(is_selected)
|
||||||
|
.selected_icon(self.selected_icon)
|
||||||
|
.size(self.icon_size)
|
||||||
|
.color(self.icon_color)
|
||||||
|
}))
|
||||||
|
.child(
|
||||||
|
Label::new(label)
|
||||||
.color(label_color)
|
.color(label_color)
|
||||||
.line_height_style(LineHeightStyle::UILabel),
|
.line_height_style(LineHeightStyle::UILabel),
|
||||||
)
|
)
|
||||||
|
|
84
crates/ui2/src/components/button/button_icon.rs
Normal file
84
crates/ui2/src/components/button/button_icon.rs
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
use crate::{prelude::*, Icon, IconElement, IconSize};
|
||||||
|
|
||||||
|
/// An icon that appears within a button.
|
||||||
|
///
|
||||||
|
/// Can be used as either an icon alongside a label, like in [`Button`](crate::Button),
|
||||||
|
/// or as a standalone icon, like in [`IconButton`](crate::IconButton).
|
||||||
|
#[derive(IntoElement)]
|
||||||
|
pub(super) struct ButtonIcon {
|
||||||
|
icon: Icon,
|
||||||
|
size: IconSize,
|
||||||
|
color: Color,
|
||||||
|
disabled: bool,
|
||||||
|
selected: bool,
|
||||||
|
selected_icon: Option<Icon>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ButtonIcon {
|
||||||
|
pub fn new(icon: Icon) -> Self {
|
||||||
|
Self {
|
||||||
|
icon,
|
||||||
|
size: IconSize::default(),
|
||||||
|
color: Color::default(),
|
||||||
|
disabled: false,
|
||||||
|
selected: false,
|
||||||
|
selected_icon: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn size(mut self, size: impl Into<Option<IconSize>>) -> Self {
|
||||||
|
if let Some(size) = size.into() {
|
||||||
|
self.size = size;
|
||||||
|
}
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn color(mut self, color: impl Into<Option<Color>>) -> Self {
|
||||||
|
if let Some(color) = color.into() {
|
||||||
|
self.color = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn selected_icon(mut self, icon: impl Into<Option<Icon>>) -> Self {
|
||||||
|
self.selected_icon = icon.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Disableable for ButtonIcon {
|
||||||
|
fn disabled(mut self, disabled: bool) -> Self {
|
||||||
|
self.disabled = disabled;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Selectable for ButtonIcon {
|
||||||
|
fn selected(mut self, selected: bool) -> Self {
|
||||||
|
self.selected = selected;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderOnce for ButtonIcon {
|
||||||
|
type Rendered = IconElement;
|
||||||
|
|
||||||
|
fn render(self, _cx: &mut WindowContext) -> Self::Rendered {
|
||||||
|
let icon = self
|
||||||
|
.selected_icon
|
||||||
|
.filter(|_| self.selected)
|
||||||
|
.unwrap_or(self.icon);
|
||||||
|
|
||||||
|
let icon_color = if self.disabled {
|
||||||
|
Color::Disabled
|
||||||
|
} else if self.selected {
|
||||||
|
Color::Selected
|
||||||
|
} else {
|
||||||
|
self.color
|
||||||
|
};
|
||||||
|
|
||||||
|
IconElement::new(icon).size(self.size).color(icon_color)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,3 +1,4 @@
|
||||||
|
use gpui::{relative, DefiniteLength};
|
||||||
use gpui::{rems, transparent_black, AnyElement, AnyView, ClickEvent, Div, Hsla, Rems, Stateful};
|
use gpui::{rems, transparent_black, AnyElement, AnyView, ClickEvent, Div, Hsla, Rems, Stateful};
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
|
|
||||||
|
@ -5,18 +6,50 @@ use crate::h_stack;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
|
||||||
pub trait ButtonCommon: Clickable + Disableable {
|
pub trait ButtonCommon: Clickable + Disableable {
|
||||||
|
/// A unique element ID to identify the button.
|
||||||
fn id(&self) -> &ElementId;
|
fn id(&self) -> &ElementId;
|
||||||
|
|
||||||
|
/// The visual style of the button.
|
||||||
|
///
|
||||||
|
/// Mosty commonly will be [`ButtonStyle::Subtle`], or [`ButtonStyle::Filled`]
|
||||||
|
/// for an emphasized button.
|
||||||
fn style(self, style: ButtonStyle) -> Self;
|
fn style(self, style: ButtonStyle) -> Self;
|
||||||
|
|
||||||
|
/// The size of the button.
|
||||||
|
///
|
||||||
|
/// Most buttons will use the default size.
|
||||||
|
///
|
||||||
|
/// [`ButtonSize`] can also be used to help build non-button elements
|
||||||
|
/// that are consistently sized with buttons.
|
||||||
fn size(self, size: ButtonSize) -> Self;
|
fn size(self, size: ButtonSize) -> Self;
|
||||||
|
|
||||||
|
/// The tooltip that shows when a user hovers over the button.
|
||||||
|
///
|
||||||
|
/// Nearly all interactable elements should have a tooltip. Some example
|
||||||
|
/// exceptions might a scroll bar, or a slider.
|
||||||
fn tooltip(self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self;
|
fn tooltip(self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)]
|
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)]
|
||||||
pub enum ButtonStyle {
|
pub enum ButtonStyle {
|
||||||
#[default]
|
/// A filled button with a solid background color. Provides emphasis versus
|
||||||
|
/// the more common subtle button.
|
||||||
Filled,
|
Filled,
|
||||||
// Tinted,
|
|
||||||
|
/// 🚧 Under construction 🚧
|
||||||
|
///
|
||||||
|
/// Used to emphasize a button in some way, like a selected state, or a semantic
|
||||||
|
/// coloring like an error or success button.
|
||||||
|
Tinted,
|
||||||
|
|
||||||
|
/// The default button style, used for most buttons. Has a transparent background,
|
||||||
|
/// but has a background color to indicate states like hover and active.
|
||||||
|
#[default]
|
||||||
Subtle,
|
Subtle,
|
||||||
|
|
||||||
|
/// Used for buttons that only change forground color on hover and active states.
|
||||||
|
///
|
||||||
|
/// TODO: Better docs for this.
|
||||||
Transparent,
|
Transparent,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,6 +73,12 @@ impl ButtonStyle {
|
||||||
label_color: Color::Default.color(cx),
|
label_color: Color::Default.color(cx),
|
||||||
icon_color: Color::Default.color(cx),
|
icon_color: Color::Default.color(cx),
|
||||||
},
|
},
|
||||||
|
ButtonStyle::Tinted => ButtonLikeStyles {
|
||||||
|
background: gpui::red(),
|
||||||
|
border_color: gpui::red(),
|
||||||
|
label_color: gpui::red(),
|
||||||
|
icon_color: gpui::red(),
|
||||||
|
},
|
||||||
ButtonStyle::Subtle => ButtonLikeStyles {
|
ButtonStyle::Subtle => ButtonLikeStyles {
|
||||||
background: cx.theme().colors().ghost_element_background,
|
background: cx.theme().colors().ghost_element_background,
|
||||||
border_color: transparent_black(),
|
border_color: transparent_black(),
|
||||||
|
@ -63,6 +102,12 @@ impl ButtonStyle {
|
||||||
label_color: Color::Default.color(cx),
|
label_color: Color::Default.color(cx),
|
||||||
icon_color: Color::Default.color(cx),
|
icon_color: Color::Default.color(cx),
|
||||||
},
|
},
|
||||||
|
ButtonStyle::Tinted => ButtonLikeStyles {
|
||||||
|
background: gpui::red(),
|
||||||
|
border_color: gpui::red(),
|
||||||
|
label_color: gpui::red(),
|
||||||
|
icon_color: gpui::red(),
|
||||||
|
},
|
||||||
ButtonStyle::Subtle => ButtonLikeStyles {
|
ButtonStyle::Subtle => ButtonLikeStyles {
|
||||||
background: cx.theme().colors().ghost_element_hover,
|
background: cx.theme().colors().ghost_element_hover,
|
||||||
border_color: transparent_black(),
|
border_color: transparent_black(),
|
||||||
|
@ -88,6 +133,12 @@ impl ButtonStyle {
|
||||||
label_color: Color::Default.color(cx),
|
label_color: Color::Default.color(cx),
|
||||||
icon_color: Color::Default.color(cx),
|
icon_color: Color::Default.color(cx),
|
||||||
},
|
},
|
||||||
|
ButtonStyle::Tinted => ButtonLikeStyles {
|
||||||
|
background: gpui::red(),
|
||||||
|
border_color: gpui::red(),
|
||||||
|
label_color: gpui::red(),
|
||||||
|
icon_color: gpui::red(),
|
||||||
|
},
|
||||||
ButtonStyle::Subtle => ButtonLikeStyles {
|
ButtonStyle::Subtle => ButtonLikeStyles {
|
||||||
background: cx.theme().colors().ghost_element_active,
|
background: cx.theme().colors().ghost_element_active,
|
||||||
border_color: transparent_black(),
|
border_color: transparent_black(),
|
||||||
|
@ -114,6 +165,12 @@ impl ButtonStyle {
|
||||||
label_color: Color::Default.color(cx),
|
label_color: Color::Default.color(cx),
|
||||||
icon_color: Color::Default.color(cx),
|
icon_color: Color::Default.color(cx),
|
||||||
},
|
},
|
||||||
|
ButtonStyle::Tinted => ButtonLikeStyles {
|
||||||
|
background: gpui::red(),
|
||||||
|
border_color: gpui::red(),
|
||||||
|
label_color: gpui::red(),
|
||||||
|
icon_color: gpui::red(),
|
||||||
|
},
|
||||||
ButtonStyle::Subtle => ButtonLikeStyles {
|
ButtonStyle::Subtle => ButtonLikeStyles {
|
||||||
background: cx.theme().colors().ghost_element_background,
|
background: cx.theme().colors().ghost_element_background,
|
||||||
border_color: cx.theme().colors().border_focused,
|
border_color: cx.theme().colors().border_focused,
|
||||||
|
@ -137,6 +194,12 @@ impl ButtonStyle {
|
||||||
label_color: Color::Disabled.color(cx),
|
label_color: Color::Disabled.color(cx),
|
||||||
icon_color: Color::Disabled.color(cx),
|
icon_color: Color::Disabled.color(cx),
|
||||||
},
|
},
|
||||||
|
ButtonStyle::Tinted => ButtonLikeStyles {
|
||||||
|
background: gpui::red(),
|
||||||
|
border_color: gpui::red(),
|
||||||
|
label_color: gpui::red(),
|
||||||
|
icon_color: gpui::red(),
|
||||||
|
},
|
||||||
ButtonStyle::Subtle => ButtonLikeStyles {
|
ButtonStyle::Subtle => ButtonLikeStyles {
|
||||||
background: cx.theme().colors().ghost_element_disabled,
|
background: cx.theme().colors().ghost_element_disabled,
|
||||||
border_color: cx.theme().colors().border_disabled,
|
border_color: cx.theme().colors().border_disabled,
|
||||||
|
@ -153,6 +216,8 @@ impl ButtonStyle {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// ButtonSize can also be used to help build non-button elements
|
||||||
|
/// that are consistently sized with buttons.
|
||||||
#[derive(Default, PartialEq, Clone, Copy)]
|
#[derive(Default, PartialEq, Clone, Copy)]
|
||||||
pub enum ButtonSize {
|
pub enum ButtonSize {
|
||||||
#[default]
|
#[default]
|
||||||
|
@ -171,12 +236,18 @@ impl ButtonSize {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A button-like element that can be used to create a custom button when
|
||||||
|
/// prebuilt buttons are not sufficient. Use this sparingly, as it is
|
||||||
|
/// unconstrained and may make the UI feel less consistent.
|
||||||
|
///
|
||||||
|
/// This is also used to build the prebuilt buttons.
|
||||||
#[derive(IntoElement)]
|
#[derive(IntoElement)]
|
||||||
pub struct ButtonLike {
|
pub struct ButtonLike {
|
||||||
id: ElementId,
|
id: ElementId,
|
||||||
pub(super) style: ButtonStyle,
|
pub(super) style: ButtonStyle,
|
||||||
pub(super) disabled: bool,
|
pub(super) disabled: bool,
|
||||||
pub(super) selected: bool,
|
pub(super) selected: bool,
|
||||||
|
pub(super) width: Option<DefiniteLength>,
|
||||||
size: ButtonSize,
|
size: ButtonSize,
|
||||||
tooltip: Option<Box<dyn Fn(&mut WindowContext) -> AnyView>>,
|
tooltip: Option<Box<dyn Fn(&mut WindowContext) -> AnyView>>,
|
||||||
on_click: Option<Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
|
on_click: Option<Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
|
||||||
|
@ -190,6 +261,7 @@ impl ButtonLike {
|
||||||
style: ButtonStyle::default(),
|
style: ButtonStyle::default(),
|
||||||
disabled: false,
|
disabled: false,
|
||||||
selected: false,
|
selected: false,
|
||||||
|
width: None,
|
||||||
size: ButtonSize::Default,
|
size: ButtonSize::Default,
|
||||||
tooltip: None,
|
tooltip: None,
|
||||||
children: SmallVec::new(),
|
children: SmallVec::new(),
|
||||||
|
@ -219,6 +291,18 @@ impl Clickable for ButtonLike {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl FixedWidth for ButtonLike {
|
||||||
|
fn width(mut self, width: DefiniteLength) -> Self {
|
||||||
|
self.width = Some(width);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn full_width(mut self) -> Self {
|
||||||
|
self.width = Some(relative(1.));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl ButtonCommon for ButtonLike {
|
impl ButtonCommon for ButtonLike {
|
||||||
fn id(&self) -> &ElementId {
|
fn id(&self) -> &ElementId {
|
||||||
&self.id
|
&self.id
|
||||||
|
@ -252,14 +336,19 @@ impl RenderOnce for ButtonLike {
|
||||||
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
||||||
h_stack()
|
h_stack()
|
||||||
.id(self.id.clone())
|
.id(self.id.clone())
|
||||||
|
.group("")
|
||||||
|
.flex_none()
|
||||||
.h(self.size.height())
|
.h(self.size.height())
|
||||||
|
.when_some(self.width, |this, width| this.w(width))
|
||||||
.rounded_md()
|
.rounded_md()
|
||||||
.cursor_pointer()
|
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.px_1()
|
.px_1()
|
||||||
.bg(self.style.enabled(cx).background)
|
.bg(self.style.enabled(cx).background)
|
||||||
|
.when(!self.disabled, |this| {
|
||||||
|
this.cursor_pointer()
|
||||||
.hover(|hover| hover.bg(self.style.hovered(cx).background))
|
.hover(|hover| hover.bg(self.style.hovered(cx).background))
|
||||||
.active(|active| active.bg(self.style.active(cx).background))
|
.active(|active| active.bg(self.style.active(cx).background))
|
||||||
|
})
|
||||||
.when_some(
|
.when_some(
|
||||||
self.on_click.filter(|_| !self.disabled),
|
self.on_click.filter(|_| !self.disabled),
|
||||||
|this, on_click| {
|
|this, on_click| {
|
||||||
|
@ -270,7 +359,11 @@ impl RenderOnce for ButtonLike {
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.when_some(self.tooltip, |this, tooltip| {
|
.when_some(self.tooltip, |this, tooltip| {
|
||||||
|
if !self.selected {
|
||||||
this.tooltip(move |cx| tooltip(cx))
|
this.tooltip(move |cx| tooltip(cx))
|
||||||
|
} else {
|
||||||
|
this
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.children(self.children)
|
.children(self.children)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
use gpui::{Action, AnyView};
|
use gpui::{Action, AnyView, DefiniteLength};
|
||||||
|
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::{ButtonCommon, ButtonLike, ButtonSize, ButtonStyle, Icon, IconElement, IconSize};
|
use crate::{ButtonCommon, ButtonLike, ButtonSize, ButtonStyle, Icon, IconSize};
|
||||||
|
|
||||||
|
use super::button_icon::ButtonIcon;
|
||||||
|
|
||||||
#[derive(IntoElement)]
|
#[derive(IntoElement)]
|
||||||
pub struct IconButton {
|
pub struct IconButton {
|
||||||
|
@ -9,6 +11,7 @@ pub struct IconButton {
|
||||||
icon: Icon,
|
icon: Icon,
|
||||||
icon_size: IconSize,
|
icon_size: IconSize,
|
||||||
icon_color: Color,
|
icon_color: Color,
|
||||||
|
selected_icon: Option<Icon>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IconButton {
|
impl IconButton {
|
||||||
|
@ -18,6 +21,7 @@ impl IconButton {
|
||||||
icon,
|
icon,
|
||||||
icon_size: IconSize::default(),
|
icon_size: IconSize::default(),
|
||||||
icon_color: Color::Default,
|
icon_color: Color::Default,
|
||||||
|
selected_icon: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,6 +35,11 @@ impl IconButton {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn selected_icon(mut self, icon: impl Into<Option<Icon>>) -> Self {
|
||||||
|
self.selected_icon = icon.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn action(self, action: Box<dyn Action>) -> Self {
|
pub fn action(self, action: Box<dyn Action>) -> Self {
|
||||||
self.on_click(move |_event, cx| cx.dispatch_action(action.boxed_clone()))
|
self.on_click(move |_event, cx| cx.dispatch_action(action.boxed_clone()))
|
||||||
}
|
}
|
||||||
|
@ -60,6 +69,18 @@ impl Clickable for IconButton {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl FixedWidth for IconButton {
|
||||||
|
fn width(mut self, width: DefiniteLength) -> Self {
|
||||||
|
self.base = self.base.width(width);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn full_width(mut self) -> Self {
|
||||||
|
self.base = self.base.full_width();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl ButtonCommon for IconButton {
|
impl ButtonCommon for IconButton {
|
||||||
fn id(&self) -> &ElementId {
|
fn id(&self) -> &ElementId {
|
||||||
self.base.id()
|
self.base.id()
|
||||||
|
@ -85,18 +106,16 @@ impl RenderOnce for IconButton {
|
||||||
type Rendered = ButtonLike;
|
type Rendered = ButtonLike;
|
||||||
|
|
||||||
fn render(self, _cx: &mut WindowContext) -> Self::Rendered {
|
fn render(self, _cx: &mut WindowContext) -> Self::Rendered {
|
||||||
let icon_color = if self.base.disabled {
|
let is_disabled = self.base.disabled;
|
||||||
Color::Disabled
|
let is_selected = self.base.selected;
|
||||||
} else if self.base.selected {
|
|
||||||
Color::Selected
|
|
||||||
} else {
|
|
||||||
self.icon_color
|
|
||||||
};
|
|
||||||
|
|
||||||
self.base.child(
|
self.base.child(
|
||||||
IconElement::new(self.icon)
|
ButtonIcon::new(self.icon)
|
||||||
|
.disabled(is_disabled)
|
||||||
|
.selected(is_selected)
|
||||||
|
.selected_icon(self.selected_icon)
|
||||||
.size(self.icon_size)
|
.size(self.icon_size)
|
||||||
.color(icon_color),
|
.color(self.icon_color),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,20 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
h_stack, prelude::*, v_stack, KeyBinding, Label, List, ListItem, ListSeparator, ListSubHeader,
|
h_stack, prelude::*, v_stack, Icon, IconElement, KeyBinding, Label, List, ListItem,
|
||||||
|
ListSeparator, ListSubHeader,
|
||||||
};
|
};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
overlay, px, Action, AnchorCorner, AnyElement, AppContext, Bounds, DismissEvent, DispatchPhase,
|
px, Action, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView,
|
||||||
Div, EventEmitter, FocusHandle, FocusableView, IntoElement, LayoutId, ManagedView, MouseButton,
|
IntoElement, Render, View, VisualContext,
|
||||||
MouseDownEvent, Pixels, Point, Render, View, VisualContext,
|
|
||||||
};
|
};
|
||||||
use menu::{SelectFirst, SelectLast, SelectNext, SelectPrev};
|
use menu::{SelectFirst, SelectLast, SelectNext, SelectPrev};
|
||||||
use std::{cell::RefCell, rc::Rc};
|
use std::rc::Rc;
|
||||||
|
|
||||||
pub enum ContextMenuItem {
|
pub enum ContextMenuItem {
|
||||||
Separator,
|
Separator,
|
||||||
Header(SharedString),
|
Header(SharedString),
|
||||||
Entry {
|
Entry {
|
||||||
label: SharedString,
|
label: SharedString,
|
||||||
|
icon: Option<Icon>,
|
||||||
handler: Rc<dyn Fn(&mut WindowContext)>,
|
handler: Rc<dyn Fn(&mut WindowContext)>,
|
||||||
key_binding: Option<KeyBinding>,
|
key_binding: Option<KeyBinding>,
|
||||||
},
|
},
|
||||||
|
@ -70,6 +71,7 @@ impl ContextMenu {
|
||||||
label: label.into(),
|
label: label.into(),
|
||||||
handler: Rc::new(on_click),
|
handler: Rc::new(on_click),
|
||||||
key_binding: None,
|
key_binding: None,
|
||||||
|
icon: None,
|
||||||
});
|
});
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
@ -84,6 +86,22 @@ impl ContextMenu {
|
||||||
label: label.into(),
|
label: label.into(),
|
||||||
key_binding: KeyBinding::for_action(&*action, cx),
|
key_binding: KeyBinding::for_action(&*action, cx),
|
||||||
handler: Rc::new(move |cx| cx.dispatch_action(action.boxed_clone())),
|
handler: Rc::new(move |cx| cx.dispatch_action(action.boxed_clone())),
|
||||||
|
icon: None,
|
||||||
|
});
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn link(
|
||||||
|
mut self,
|
||||||
|
label: impl Into<SharedString>,
|
||||||
|
action: Box<dyn Action>,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) -> Self {
|
||||||
|
self.items.push(ContextMenuItem::Entry {
|
||||||
|
label: label.into(),
|
||||||
|
key_binding: KeyBinding::for_action(&*action, cx),
|
||||||
|
handler: Rc::new(move |cx| cx.dispatch_action(action.boxed_clone())),
|
||||||
|
icon: Some(Icon::Link),
|
||||||
});
|
});
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
@ -176,19 +194,30 @@ impl Render for ContextMenu {
|
||||||
ListSubHeader::new(header.clone()).into_any_element()
|
ListSubHeader::new(header.clone()).into_any_element()
|
||||||
}
|
}
|
||||||
ContextMenuItem::Entry {
|
ContextMenuItem::Entry {
|
||||||
label: entry,
|
label,
|
||||||
handler: callback,
|
handler,
|
||||||
key_binding,
|
key_binding,
|
||||||
|
icon,
|
||||||
} => {
|
} => {
|
||||||
let callback = callback.clone();
|
let handler = handler.clone();
|
||||||
let dismiss = cx.listener(|_, _, cx| cx.emit(DismissEvent));
|
let dismiss = cx.listener(|_, _, cx| cx.emit(DismissEvent));
|
||||||
|
|
||||||
ListItem::new(entry.clone())
|
let label_element = if let Some(icon) = icon {
|
||||||
|
h_stack()
|
||||||
|
.gap_1()
|
||||||
|
.child(Label::new(label.clone()))
|
||||||
|
.child(IconElement::new(*icon))
|
||||||
|
.into_any_element()
|
||||||
|
} else {
|
||||||
|
Label::new(label.clone()).into_any_element()
|
||||||
|
};
|
||||||
|
|
||||||
|
ListItem::new(label.clone())
|
||||||
.child(
|
.child(
|
||||||
h_stack()
|
h_stack()
|
||||||
.w_full()
|
.w_full()
|
||||||
.justify_between()
|
.justify_between()
|
||||||
.child(Label::new(entry.clone()))
|
.child(label_element)
|
||||||
.children(
|
.children(
|
||||||
key_binding
|
key_binding
|
||||||
.clone()
|
.clone()
|
||||||
|
@ -197,7 +226,7 @@ impl Render for ContextMenu {
|
||||||
)
|
)
|
||||||
.selected(Some(ix) == self.selected_index)
|
.selected(Some(ix) == self.selected_index)
|
||||||
.on_click(move |event, cx| {
|
.on_click(move |event, cx| {
|
||||||
callback(cx);
|
handler(cx);
|
||||||
dismiss(event, cx)
|
dismiss(event, cx)
|
||||||
})
|
})
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
|
@ -208,174 +237,3 @@ impl Render for ContextMenu {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct MenuHandle<M: ManagedView> {
|
|
||||||
id: ElementId,
|
|
||||||
child_builder: Option<Box<dyn FnOnce(bool) -> AnyElement + 'static>>,
|
|
||||||
menu_builder: Option<Rc<dyn Fn(&mut WindowContext) -> View<M> + 'static>>,
|
|
||||||
anchor: Option<AnchorCorner>,
|
|
||||||
attach: Option<AnchorCorner>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<M: ManagedView> MenuHandle<M> {
|
|
||||||
pub fn menu(mut self, f: impl Fn(&mut WindowContext) -> View<M> + 'static) -> Self {
|
|
||||||
self.menu_builder = Some(Rc::new(f));
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn child<R: IntoElement>(mut self, f: impl FnOnce(bool) -> R + 'static) -> Self {
|
|
||||||
self.child_builder = Some(Box::new(|b| f(b).into_element().into_any()));
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// anchor defines which corner of the menu to anchor to the attachment point
|
|
||||||
/// (by default the cursor position, but see attach)
|
|
||||||
pub fn anchor(mut self, anchor: AnchorCorner) -> Self {
|
|
||||||
self.anchor = Some(anchor);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// attach defines which corner of the handle to attach the menu's anchor to
|
|
||||||
pub fn attach(mut self, attach: AnchorCorner) -> Self {
|
|
||||||
self.attach = Some(attach);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn menu_handle<M: ManagedView>(id: impl Into<ElementId>) -> MenuHandle<M> {
|
|
||||||
MenuHandle {
|
|
||||||
id: id.into(),
|
|
||||||
child_builder: None,
|
|
||||||
menu_builder: None,
|
|
||||||
anchor: None,
|
|
||||||
attach: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct MenuHandleState<M> {
|
|
||||||
menu: Rc<RefCell<Option<View<M>>>>,
|
|
||||||
position: Rc<RefCell<Point<Pixels>>>,
|
|
||||||
child_layout_id: Option<LayoutId>,
|
|
||||||
child_element: Option<AnyElement>,
|
|
||||||
menu_element: Option<AnyElement>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<M: ManagedView> Element for MenuHandle<M> {
|
|
||||||
type State = MenuHandleState<M>;
|
|
||||||
|
|
||||||
fn layout(
|
|
||||||
&mut self,
|
|
||||||
element_state: Option<Self::State>,
|
|
||||||
cx: &mut WindowContext,
|
|
||||||
) -> (gpui::LayoutId, Self::State) {
|
|
||||||
let (menu, position) = if let Some(element_state) = element_state {
|
|
||||||
(element_state.menu, element_state.position)
|
|
||||||
} else {
|
|
||||||
(Rc::default(), Rc::default())
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut menu_layout_id = None;
|
|
||||||
|
|
||||||
let menu_element = menu.borrow_mut().as_mut().map(|menu| {
|
|
||||||
let mut overlay = overlay().snap_to_window();
|
|
||||||
if let Some(anchor) = self.anchor {
|
|
||||||
overlay = overlay.anchor(anchor);
|
|
||||||
}
|
|
||||||
overlay = overlay.position(*position.borrow());
|
|
||||||
|
|
||||||
let mut element = overlay.child(menu.clone()).into_any();
|
|
||||||
menu_layout_id = Some(element.layout(cx));
|
|
||||||
element
|
|
||||||
});
|
|
||||||
|
|
||||||
let mut child_element = self
|
|
||||||
.child_builder
|
|
||||||
.take()
|
|
||||||
.map(|child_builder| (child_builder)(menu.borrow().is_some()));
|
|
||||||
|
|
||||||
let child_layout_id = child_element
|
|
||||||
.as_mut()
|
|
||||||
.map(|child_element| child_element.layout(cx));
|
|
||||||
|
|
||||||
let layout_id = cx.request_layout(
|
|
||||||
&gpui::Style::default(),
|
|
||||||
menu_layout_id.into_iter().chain(child_layout_id),
|
|
||||||
);
|
|
||||||
|
|
||||||
(
|
|
||||||
layout_id,
|
|
||||||
MenuHandleState {
|
|
||||||
menu,
|
|
||||||
position,
|
|
||||||
child_element,
|
|
||||||
child_layout_id,
|
|
||||||
menu_element,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn paint(
|
|
||||||
self,
|
|
||||||
bounds: Bounds<gpui::Pixels>,
|
|
||||||
element_state: &mut Self::State,
|
|
||||||
cx: &mut WindowContext,
|
|
||||||
) {
|
|
||||||
if let Some(child) = element_state.child_element.take() {
|
|
||||||
child.paint(cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(menu) = element_state.menu_element.take() {
|
|
||||||
menu.paint(cx);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let Some(builder) = self.menu_builder else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let menu = element_state.menu.clone();
|
|
||||||
let position = element_state.position.clone();
|
|
||||||
let attach = self.attach.clone();
|
|
||||||
let child_layout_id = element_state.child_layout_id.clone();
|
|
||||||
|
|
||||||
cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| {
|
|
||||||
if phase == DispatchPhase::Bubble
|
|
||||||
&& event.button == MouseButton::Right
|
|
||||||
&& bounds.contains_point(&event.position)
|
|
||||||
{
|
|
||||||
cx.stop_propagation();
|
|
||||||
cx.prevent_default();
|
|
||||||
|
|
||||||
let new_menu = (builder)(cx);
|
|
||||||
let menu2 = menu.clone();
|
|
||||||
cx.subscribe(&new_menu, move |_modal, _: &DismissEvent, cx| {
|
|
||||||
*menu2.borrow_mut() = None;
|
|
||||||
cx.notify();
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
cx.focus_view(&new_menu);
|
|
||||||
*menu.borrow_mut() = Some(new_menu);
|
|
||||||
|
|
||||||
*position.borrow_mut() = if attach.is_some() && child_layout_id.is_some() {
|
|
||||||
attach
|
|
||||||
.unwrap()
|
|
||||||
.corner(cx.layout_bounds(child_layout_id.unwrap()))
|
|
||||||
} else {
|
|
||||||
cx.mouse_position()
|
|
||||||
};
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<M: ManagedView> IntoElement for MenuHandle<M> {
|
|
||||||
type Element = Self;
|
|
||||||
|
|
||||||
fn element_id(&self) -> Option<gpui::ElementId> {
|
|
||||||
Some(self.id.clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn into_element(self) -> Self::Element {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -27,6 +27,7 @@ pub enum Icon {
|
||||||
Bolt,
|
Bolt,
|
||||||
CaseSensitive,
|
CaseSensitive,
|
||||||
Check,
|
Check,
|
||||||
|
Copy,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
|
@ -54,6 +55,7 @@ pub enum Icon {
|
||||||
FolderX,
|
FolderX,
|
||||||
Hash,
|
Hash,
|
||||||
InlayHint,
|
InlayHint,
|
||||||
|
Link,
|
||||||
MagicWand,
|
MagicWand,
|
||||||
MagnifyingGlass,
|
MagnifyingGlass,
|
||||||
MailOpen,
|
MailOpen,
|
||||||
|
@ -99,6 +101,7 @@ impl Icon {
|
||||||
Icon::Bolt => "icons/bolt.svg",
|
Icon::Bolt => "icons/bolt.svg",
|
||||||
Icon::CaseSensitive => "icons/case_insensitive.svg",
|
Icon::CaseSensitive => "icons/case_insensitive.svg",
|
||||||
Icon::Check => "icons/check.svg",
|
Icon::Check => "icons/check.svg",
|
||||||
|
Icon::Copy => "icons/copy.svg",
|
||||||
Icon::ChevronDown => "icons/chevron_down.svg",
|
Icon::ChevronDown => "icons/chevron_down.svg",
|
||||||
Icon::ChevronLeft => "icons/chevron_left.svg",
|
Icon::ChevronLeft => "icons/chevron_left.svg",
|
||||||
Icon::ChevronRight => "icons/chevron_right.svg",
|
Icon::ChevronRight => "icons/chevron_right.svg",
|
||||||
|
@ -126,6 +129,7 @@ impl Icon {
|
||||||
Icon::FolderX => "icons/stop_sharing.svg",
|
Icon::FolderX => "icons/stop_sharing.svg",
|
||||||
Icon::Hash => "icons/hash.svg",
|
Icon::Hash => "icons/hash.svg",
|
||||||
Icon::InlayHint => "icons/inlay_hint.svg",
|
Icon::InlayHint => "icons/inlay_hint.svg",
|
||||||
|
Icon::Link => "icons/link.svg",
|
||||||
Icon::MagicWand => "icons/magic-wand.svg",
|
Icon::MagicWand => "icons/magic-wand.svg",
|
||||||
Icon::MagnifyingGlass => "icons/magnifying_glass.svg",
|
Icon::MagnifyingGlass => "icons/magnifying_glass.svg",
|
||||||
Icon::MailOpen => "icons/mail-open.svg",
|
Icon::MailOpen => "icons/mail-open.svg",
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
|
use std::ops::Range;
|
||||||
|
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::styled_ext::StyledExt;
|
use crate::styled_ext::StyledExt;
|
||||||
use gpui::{relative, Div, IntoElement, StyledText, TextRun, WindowContext};
|
use gpui::{relative, Div, HighlightStyle, IntoElement, StyledText, WindowContext};
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)]
|
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)]
|
||||||
pub enum LabelSize {
|
pub enum LabelSize {
|
||||||
|
@ -99,38 +101,32 @@ impl RenderOnce for HighlightedLabel {
|
||||||
|
|
||||||
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
||||||
let highlight_color = cx.theme().colors().text_accent;
|
let highlight_color = cx.theme().colors().text_accent;
|
||||||
let mut text_style = cx.text_style().clone();
|
|
||||||
|
|
||||||
let mut highlight_indices = self.highlight_indices.iter().copied().peekable();
|
let mut highlight_indices = self.highlight_indices.iter().copied().peekable();
|
||||||
|
let mut highlights: Vec<(Range<usize>, HighlightStyle)> = Vec::new();
|
||||||
|
|
||||||
let mut runs: Vec<TextRun> = Vec::new();
|
while let Some(start_ix) = highlight_indices.next() {
|
||||||
|
let mut end_ix = start_ix;
|
||||||
|
|
||||||
for (char_ix, char) in self.label.char_indices() {
|
loop {
|
||||||
let mut color = self.color.color(cx);
|
end_ix = end_ix + self.label[end_ix..].chars().next().unwrap().len_utf8();
|
||||||
|
if let Some(&next_ix) = highlight_indices.peek() {
|
||||||
if let Some(highlight_ix) = highlight_indices.peek() {
|
if next_ix == end_ix {
|
||||||
if char_ix == *highlight_ix {
|
end_ix = next_ix;
|
||||||
color = highlight_color;
|
|
||||||
highlight_indices.next();
|
highlight_indices.next();
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
let last_run = runs.last_mut();
|
highlights.push((
|
||||||
let start_new_run = if let Some(last_run) = last_run {
|
start_ix..end_ix,
|
||||||
if color == last_run.color {
|
HighlightStyle {
|
||||||
last_run.len += char.len_utf8();
|
color: Some(highlight_color),
|
||||||
false
|
..Default::default()
|
||||||
} else {
|
},
|
||||||
true
|
));
|
||||||
}
|
|
||||||
} else {
|
|
||||||
true
|
|
||||||
};
|
|
||||||
|
|
||||||
if start_new_run {
|
|
||||||
text_style.color = color;
|
|
||||||
runs.push(text_style.to_run(char.len_utf8()))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
div()
|
div()
|
||||||
|
@ -150,7 +146,7 @@ impl RenderOnce for HighlightedLabel {
|
||||||
LabelSize::Default => this.text_ui(),
|
LabelSize::Default => this.text_ui(),
|
||||||
LabelSize::Small => this.text_ui_sm(),
|
LabelSize::Small => this.text_ui_sm(),
|
||||||
})
|
})
|
||||||
.child(StyledText::new(self.label).with_runs(runs))
|
.child(StyledText::new(self.label).with_highlights(&cx.text_style(), highlights))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
231
crates/ui2/src/components/popover_menu.rs
Normal file
231
crates/ui2/src/components/popover_menu.rs
Normal file
|
@ -0,0 +1,231 @@
|
||||||
|
use std::{cell::RefCell, rc::Rc};
|
||||||
|
|
||||||
|
use gpui::{
|
||||||
|
overlay, point, px, rems, AnchorCorner, AnyElement, Bounds, DismissEvent, DispatchPhase,
|
||||||
|
Element, ElementId, InteractiveBounds, IntoElement, LayoutId, ManagedView, MouseDownEvent,
|
||||||
|
ParentElement, Pixels, Point, View, VisualContext, WindowContext,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{Clickable, Selectable};
|
||||||
|
|
||||||
|
pub trait PopoverTrigger: IntoElement + Clickable + Selectable + 'static {}
|
||||||
|
|
||||||
|
impl<T: IntoElement + Clickable + Selectable + 'static> PopoverTrigger for T {}
|
||||||
|
|
||||||
|
pub struct PopoverMenu<M: ManagedView> {
|
||||||
|
id: ElementId,
|
||||||
|
child_builder: Option<
|
||||||
|
Box<
|
||||||
|
dyn FnOnce(
|
||||||
|
Rc<RefCell<Option<View<M>>>>,
|
||||||
|
Option<Rc<dyn Fn(&mut WindowContext) -> View<M> + 'static>>,
|
||||||
|
) -> AnyElement
|
||||||
|
+ 'static,
|
||||||
|
>,
|
||||||
|
>,
|
||||||
|
menu_builder: Option<Rc<dyn Fn(&mut WindowContext) -> View<M> + 'static>>,
|
||||||
|
anchor: AnchorCorner,
|
||||||
|
attach: Option<AnchorCorner>,
|
||||||
|
offset: Option<Point<Pixels>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<M: ManagedView> PopoverMenu<M> {
|
||||||
|
pub fn menu(mut self, f: impl Fn(&mut WindowContext) -> View<M> + 'static) -> Self {
|
||||||
|
self.menu_builder = Some(Rc::new(f));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn trigger<T: PopoverTrigger>(mut self, t: T) -> Self {
|
||||||
|
self.child_builder = Some(Box::new(|menu, builder| {
|
||||||
|
let open = menu.borrow().is_some();
|
||||||
|
t.selected(open)
|
||||||
|
.when_some(builder, |el, builder| {
|
||||||
|
el.on_click({
|
||||||
|
move |_, cx| {
|
||||||
|
let new_menu = (builder)(cx);
|
||||||
|
let menu2 = menu.clone();
|
||||||
|
let previous_focus_handle = cx.focused();
|
||||||
|
|
||||||
|
cx.subscribe(&new_menu, move |modal, _: &DismissEvent, cx| {
|
||||||
|
if modal.focus_handle(cx).contains_focused(cx) {
|
||||||
|
if previous_focus_handle.is_some() {
|
||||||
|
cx.focus(&previous_focus_handle.as_ref().unwrap())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*menu2.borrow_mut() = None;
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
cx.focus_view(&new_menu);
|
||||||
|
*menu.borrow_mut() = Some(new_menu);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.into_any_element()
|
||||||
|
}));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// anchor defines which corner of the menu to anchor to the attachment point
|
||||||
|
/// (by default the cursor position, but see attach)
|
||||||
|
pub fn anchor(mut self, anchor: AnchorCorner) -> Self {
|
||||||
|
self.anchor = anchor;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// attach defines which corner of the handle to attach the menu's anchor to
|
||||||
|
pub fn attach(mut self, attach: AnchorCorner) -> Self {
|
||||||
|
self.attach = Some(attach);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// offset offsets the position of the content by that many pixels.
|
||||||
|
pub fn offset(mut self, offset: Point<Pixels>) -> Self {
|
||||||
|
self.offset = Some(offset);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolved_attach(&self) -> AnchorCorner {
|
||||||
|
self.attach.unwrap_or_else(|| match self.anchor {
|
||||||
|
AnchorCorner::TopLeft => AnchorCorner::BottomLeft,
|
||||||
|
AnchorCorner::TopRight => AnchorCorner::BottomRight,
|
||||||
|
AnchorCorner::BottomLeft => AnchorCorner::TopLeft,
|
||||||
|
AnchorCorner::BottomRight => AnchorCorner::TopRight,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolved_offset(&self, cx: &WindowContext) -> Point<Pixels> {
|
||||||
|
self.offset.unwrap_or_else(|| {
|
||||||
|
// Default offset = 4px padding + 1px border
|
||||||
|
let offset = rems(5. / 16.) * cx.rem_size();
|
||||||
|
match self.anchor {
|
||||||
|
AnchorCorner::TopRight | AnchorCorner::BottomRight => point(offset, px(0.)),
|
||||||
|
AnchorCorner::TopLeft | AnchorCorner::BottomLeft => point(-offset, px(0.)),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn popover_menu<M: ManagedView>(id: impl Into<ElementId>) -> PopoverMenu<M> {
|
||||||
|
PopoverMenu {
|
||||||
|
id: id.into(),
|
||||||
|
child_builder: None,
|
||||||
|
menu_builder: None,
|
||||||
|
anchor: AnchorCorner::TopLeft,
|
||||||
|
attach: None,
|
||||||
|
offset: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct PopoverMenuState<M> {
|
||||||
|
child_layout_id: Option<LayoutId>,
|
||||||
|
child_element: Option<AnyElement>,
|
||||||
|
child_bounds: Option<Bounds<Pixels>>,
|
||||||
|
menu_element: Option<AnyElement>,
|
||||||
|
menu: Rc<RefCell<Option<View<M>>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<M: ManagedView> Element for PopoverMenu<M> {
|
||||||
|
type State = PopoverMenuState<M>;
|
||||||
|
|
||||||
|
fn layout(
|
||||||
|
&mut self,
|
||||||
|
element_state: Option<Self::State>,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) -> (gpui::LayoutId, Self::State) {
|
||||||
|
let mut menu_layout_id = None;
|
||||||
|
|
||||||
|
let (menu, child_bounds) = if let Some(element_state) = element_state {
|
||||||
|
(element_state.menu, element_state.child_bounds)
|
||||||
|
} else {
|
||||||
|
(Rc::default(), None)
|
||||||
|
};
|
||||||
|
|
||||||
|
let menu_element = menu.borrow_mut().as_mut().map(|menu| {
|
||||||
|
let mut overlay = overlay().snap_to_window().anchor(self.anchor);
|
||||||
|
|
||||||
|
if let Some(child_bounds) = child_bounds {
|
||||||
|
overlay = overlay.position(
|
||||||
|
self.resolved_attach().corner(child_bounds) + self.resolved_offset(cx),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut element = overlay.child(menu.clone()).into_any();
|
||||||
|
menu_layout_id = Some(element.layout(cx));
|
||||||
|
element
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut child_element = self
|
||||||
|
.child_builder
|
||||||
|
.take()
|
||||||
|
.map(|child_builder| (child_builder)(menu.clone(), self.menu_builder.clone()));
|
||||||
|
|
||||||
|
let child_layout_id = child_element
|
||||||
|
.as_mut()
|
||||||
|
.map(|child_element| child_element.layout(cx));
|
||||||
|
|
||||||
|
let layout_id = cx.request_layout(
|
||||||
|
&gpui::Style::default(),
|
||||||
|
menu_layout_id.into_iter().chain(child_layout_id),
|
||||||
|
);
|
||||||
|
|
||||||
|
(
|
||||||
|
layout_id,
|
||||||
|
PopoverMenuState {
|
||||||
|
menu,
|
||||||
|
child_element,
|
||||||
|
child_layout_id,
|
||||||
|
menu_element,
|
||||||
|
child_bounds,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paint(
|
||||||
|
self,
|
||||||
|
_: Bounds<gpui::Pixels>,
|
||||||
|
element_state: &mut Self::State,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) {
|
||||||
|
if let Some(child) = element_state.child_element.take() {
|
||||||
|
child.paint(cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(child_layout_id) = element_state.child_layout_id.take() {
|
||||||
|
element_state.child_bounds = Some(cx.layout_bounds(child_layout_id));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(menu) = element_state.menu_element.take() {
|
||||||
|
menu.paint(cx);
|
||||||
|
|
||||||
|
if let Some(child_bounds) = element_state.child_bounds {
|
||||||
|
let interactive_bounds = InteractiveBounds {
|
||||||
|
bounds: child_bounds,
|
||||||
|
stacking_order: cx.stacking_order().clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mouse-downing outside the menu dismisses it, so we don't
|
||||||
|
// want a click on the toggle to re-open it.
|
||||||
|
cx.on_mouse_event(move |e: &MouseDownEvent, phase, cx| {
|
||||||
|
if phase == DispatchPhase::Bubble
|
||||||
|
&& interactive_bounds.visibly_contains(&e.position, cx)
|
||||||
|
{
|
||||||
|
cx.stop_propagation()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<M: ManagedView> IntoElement for PopoverMenu<M> {
|
||||||
|
type Element = Self;
|
||||||
|
|
||||||
|
fn element_id(&self) -> Option<gpui::ElementId> {
|
||||||
|
Some(self.id.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn into_element(self) -> Self::Element {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
185
crates/ui2/src/components/right_click_menu.rs
Normal file
185
crates/ui2/src/components/right_click_menu.rs
Normal file
|
@ -0,0 +1,185 @@
|
||||||
|
use std::{cell::RefCell, rc::Rc};
|
||||||
|
|
||||||
|
use gpui::{
|
||||||
|
overlay, AnchorCorner, AnyElement, Bounds, DismissEvent, DispatchPhase, Element, ElementId,
|
||||||
|
IntoElement, LayoutId, ManagedView, MouseButton, MouseDownEvent, ParentElement, Pixels, Point,
|
||||||
|
View, VisualContext, WindowContext,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct RightClickMenu<M: ManagedView> {
|
||||||
|
id: ElementId,
|
||||||
|
child_builder: Option<Box<dyn FnOnce(bool) -> AnyElement + 'static>>,
|
||||||
|
menu_builder: Option<Rc<dyn Fn(&mut WindowContext) -> View<M> + 'static>>,
|
||||||
|
anchor: Option<AnchorCorner>,
|
||||||
|
attach: Option<AnchorCorner>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<M: ManagedView> RightClickMenu<M> {
|
||||||
|
pub fn menu(mut self, f: impl Fn(&mut WindowContext) -> View<M> + 'static) -> Self {
|
||||||
|
self.menu_builder = Some(Rc::new(f));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn trigger<E: IntoElement + 'static>(mut self, e: E) -> Self {
|
||||||
|
self.child_builder = Some(Box::new(move |_| e.into_any_element()));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// anchor defines which corner of the menu to anchor to the attachment point
|
||||||
|
/// (by default the cursor position, but see attach)
|
||||||
|
pub fn anchor(mut self, anchor: AnchorCorner) -> Self {
|
||||||
|
self.anchor = Some(anchor);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// attach defines which corner of the handle to attach the menu's anchor to
|
||||||
|
pub fn attach(mut self, attach: AnchorCorner) -> Self {
|
||||||
|
self.attach = Some(attach);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn right_click_menu<M: ManagedView>(id: impl Into<ElementId>) -> RightClickMenu<M> {
|
||||||
|
RightClickMenu {
|
||||||
|
id: id.into(),
|
||||||
|
child_builder: None,
|
||||||
|
menu_builder: None,
|
||||||
|
anchor: None,
|
||||||
|
attach: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MenuHandleState<M> {
|
||||||
|
menu: Rc<RefCell<Option<View<M>>>>,
|
||||||
|
position: Rc<RefCell<Point<Pixels>>>,
|
||||||
|
child_layout_id: Option<LayoutId>,
|
||||||
|
child_element: Option<AnyElement>,
|
||||||
|
menu_element: Option<AnyElement>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<M: ManagedView> Element for RightClickMenu<M> {
|
||||||
|
type State = MenuHandleState<M>;
|
||||||
|
|
||||||
|
fn layout(
|
||||||
|
&mut self,
|
||||||
|
element_state: Option<Self::State>,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) -> (gpui::LayoutId, Self::State) {
|
||||||
|
let (menu, position) = if let Some(element_state) = element_state {
|
||||||
|
(element_state.menu, element_state.position)
|
||||||
|
} else {
|
||||||
|
(Rc::default(), Rc::default())
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut menu_layout_id = None;
|
||||||
|
|
||||||
|
let menu_element = menu.borrow_mut().as_mut().map(|menu| {
|
||||||
|
let mut overlay = overlay().snap_to_window();
|
||||||
|
if let Some(anchor) = self.anchor {
|
||||||
|
overlay = overlay.anchor(anchor);
|
||||||
|
}
|
||||||
|
overlay = overlay.position(*position.borrow());
|
||||||
|
|
||||||
|
let mut element = overlay.child(menu.clone()).into_any();
|
||||||
|
menu_layout_id = Some(element.layout(cx));
|
||||||
|
element
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut child_element = self
|
||||||
|
.child_builder
|
||||||
|
.take()
|
||||||
|
.map(|child_builder| (child_builder)(menu.borrow().is_some()));
|
||||||
|
|
||||||
|
let child_layout_id = child_element
|
||||||
|
.as_mut()
|
||||||
|
.map(|child_element| child_element.layout(cx));
|
||||||
|
|
||||||
|
let layout_id = cx.request_layout(
|
||||||
|
&gpui::Style::default(),
|
||||||
|
menu_layout_id.into_iter().chain(child_layout_id),
|
||||||
|
);
|
||||||
|
|
||||||
|
(
|
||||||
|
layout_id,
|
||||||
|
MenuHandleState {
|
||||||
|
menu,
|
||||||
|
position,
|
||||||
|
child_element,
|
||||||
|
child_layout_id,
|
||||||
|
menu_element,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paint(
|
||||||
|
self,
|
||||||
|
bounds: Bounds<gpui::Pixels>,
|
||||||
|
element_state: &mut Self::State,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) {
|
||||||
|
if let Some(child) = element_state.child_element.take() {
|
||||||
|
child.paint(cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(menu) = element_state.menu_element.take() {
|
||||||
|
menu.paint(cx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(builder) = self.menu_builder else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let menu = element_state.menu.clone();
|
||||||
|
let position = element_state.position.clone();
|
||||||
|
let attach = self.attach.clone();
|
||||||
|
let child_layout_id = element_state.child_layout_id.clone();
|
||||||
|
|
||||||
|
cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| {
|
||||||
|
if phase == DispatchPhase::Bubble
|
||||||
|
&& event.button == MouseButton::Right
|
||||||
|
&& bounds.contains_point(&event.position)
|
||||||
|
{
|
||||||
|
cx.stop_propagation();
|
||||||
|
cx.prevent_default();
|
||||||
|
|
||||||
|
let new_menu = (builder)(cx);
|
||||||
|
let menu2 = menu.clone();
|
||||||
|
let previous_focus_handle = cx.focused();
|
||||||
|
|
||||||
|
cx.subscribe(&new_menu, move |modal, _: &DismissEvent, cx| {
|
||||||
|
if modal.focus_handle(cx).contains_focused(cx) {
|
||||||
|
if previous_focus_handle.is_some() {
|
||||||
|
cx.focus(&previous_focus_handle.as_ref().unwrap())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*menu2.borrow_mut() = None;
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
cx.focus_view(&new_menu);
|
||||||
|
*menu.borrow_mut() = Some(new_menu);
|
||||||
|
|
||||||
|
*position.borrow_mut() = if attach.is_some() && child_layout_id.is_some() {
|
||||||
|
attach
|
||||||
|
.unwrap()
|
||||||
|
.corner(cx.layout_bounds(child_layout_id.unwrap()))
|
||||||
|
} else {
|
||||||
|
cx.mouse_position()
|
||||||
|
};
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<M: ManagedView> IntoElement for RightClickMenu<M> {
|
||||||
|
type Element = Self;
|
||||||
|
|
||||||
|
fn element_id(&self) -> Option<gpui::ElementId> {
|
||||||
|
Some(self.id.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn into_element(self) -> Self::Element {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
use gpui::{Div, Render};
|
use gpui::{Div, Render};
|
||||||
use story::Story;
|
use story::Story;
|
||||||
|
|
||||||
use crate::prelude::*;
|
use crate::{prelude::*, Icon};
|
||||||
use crate::{Button, ButtonStyle};
|
use crate::{Button, ButtonStyle};
|
||||||
|
|
||||||
pub struct ButtonStory;
|
pub struct ButtonStory;
|
||||||
|
@ -16,8 +16,22 @@ impl Render for ButtonStory {
|
||||||
.child(Button::new("default_filled", "Click me"))
|
.child(Button::new("default_filled", "Click me"))
|
||||||
.child(Story::label("Selected"))
|
.child(Story::label("Selected"))
|
||||||
.child(Button::new("selected_filled", "Click me").selected(true))
|
.child(Button::new("selected_filled", "Click me").selected(true))
|
||||||
|
.child(Story::label("Selected with `selected_label`"))
|
||||||
|
.child(
|
||||||
|
Button::new("selected_label_filled", "Click me")
|
||||||
|
.selected(true)
|
||||||
|
.selected_label("I have been selected"),
|
||||||
|
)
|
||||||
.child(Story::label("With `label_color`"))
|
.child(Story::label("With `label_color`"))
|
||||||
.child(Button::new("filled_with_label_color", "Click me").color(Color::Created))
|
.child(Button::new("filled_with_label_color", "Click me").color(Color::Created))
|
||||||
|
.child(Story::label("With `icon`"))
|
||||||
|
.child(Button::new("filled_with_icon", "Click me").icon(Icon::FileGit))
|
||||||
|
.child(Story::label("Selected with `icon`"))
|
||||||
|
.child(
|
||||||
|
Button::new("filled_and_selected_with_icon", "Click me")
|
||||||
|
.selected(true)
|
||||||
|
.icon(Icon::FileGit),
|
||||||
|
)
|
||||||
.child(Story::label("Default (Subtle)"))
|
.child(Story::label("Default (Subtle)"))
|
||||||
.child(Button::new("default_subtle", "Click me").style(ButtonStyle::Subtle))
|
.child(Button::new("default_subtle", "Click me").style(ButtonStyle::Subtle))
|
||||||
.child(Story::label("Default (Transparent)"))
|
.child(Story::label("Default (Transparent)"))
|
||||||
|
|
|
@ -2,7 +2,7 @@ use gpui::{actions, Action, AnchorCorner, Div, Render, View};
|
||||||
use story::Story;
|
use story::Story;
|
||||||
|
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::{menu_handle, ContextMenu, Label};
|
use crate::{right_click_menu, ContextMenu, Label};
|
||||||
|
|
||||||
actions!(PrintCurrentDate, PrintBestFood);
|
actions!(PrintCurrentDate, PrintBestFood);
|
||||||
|
|
||||||
|
@ -45,25 +45,13 @@ impl Render for ContextMenuStory {
|
||||||
.flex_col()
|
.flex_col()
|
||||||
.justify_between()
|
.justify_between()
|
||||||
.child(
|
.child(
|
||||||
menu_handle("test2")
|
right_click_menu("test2")
|
||||||
.child(|is_open| {
|
.trigger(Label::new("TOP LEFT"))
|
||||||
Label::new(if is_open {
|
|
||||||
"TOP LEFT"
|
|
||||||
} else {
|
|
||||||
"RIGHT CLICK ME"
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.menu(move |cx| build_menu(cx, "top left")),
|
.menu(move |cx| build_menu(cx, "top left")),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
menu_handle("test1")
|
right_click_menu("test1")
|
||||||
.child(|is_open| {
|
.trigger(Label::new("BOTTOM LEFT"))
|
||||||
Label::new(if is_open {
|
|
||||||
"BOTTOM LEFT"
|
|
||||||
} else {
|
|
||||||
"RIGHT CLICK ME"
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.anchor(AnchorCorner::BottomLeft)
|
.anchor(AnchorCorner::BottomLeft)
|
||||||
.attach(AnchorCorner::TopLeft)
|
.attach(AnchorCorner::TopLeft)
|
||||||
.menu(move |cx| build_menu(cx, "bottom left")),
|
.menu(move |cx| build_menu(cx, "bottom left")),
|
||||||
|
@ -75,26 +63,14 @@ impl Render for ContextMenuStory {
|
||||||
.flex_col()
|
.flex_col()
|
||||||
.justify_between()
|
.justify_between()
|
||||||
.child(
|
.child(
|
||||||
menu_handle("test3")
|
right_click_menu("test3")
|
||||||
.child(|is_open| {
|
.trigger(Label::new("TOP RIGHT"))
|
||||||
Label::new(if is_open {
|
|
||||||
"TOP RIGHT"
|
|
||||||
} else {
|
|
||||||
"RIGHT CLICK ME"
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.anchor(AnchorCorner::TopRight)
|
.anchor(AnchorCorner::TopRight)
|
||||||
.menu(move |cx| build_menu(cx, "top right")),
|
.menu(move |cx| build_menu(cx, "top right")),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
menu_handle("test4")
|
right_click_menu("test4")
|
||||||
.child(|is_open| {
|
.trigger(Label::new("BOTTOM RIGHT"))
|
||||||
Label::new(if is_open {
|
|
||||||
"BOTTOM RIGHT"
|
|
||||||
} else {
|
|
||||||
"RIGHT CLICK ME"
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.anchor(AnchorCorner::BottomRight)
|
.anchor(AnchorCorner::BottomRight)
|
||||||
.attach(AnchorCorner::TopRight)
|
.attach(AnchorCorner::TopRight)
|
||||||
.menu(move |cx| build_menu(cx, "bottom right")),
|
.menu(move |cx| build_menu(cx, "bottom right")),
|
||||||
|
|
|
@ -20,6 +20,14 @@ impl Render for IconButtonStory {
|
||||||
.w_8()
|
.w_8()
|
||||||
.child(IconButton::new("icon_a", Icon::Hash).selected(true)),
|
.child(IconButton::new("icon_a", Icon::Hash).selected(true)),
|
||||||
)
|
)
|
||||||
|
.child(Story::label("Selected with `selected_icon`"))
|
||||||
|
.child(
|
||||||
|
div().w_8().child(
|
||||||
|
IconButton::new("icon_a", Icon::AudioOn)
|
||||||
|
.selected(true)
|
||||||
|
.selected_icon(Icon::AudioOff),
|
||||||
|
),
|
||||||
|
)
|
||||||
.child(Story::label("Disabled"))
|
.child(Story::label("Disabled"))
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
use gpui::{px, Styled, WindowContext};
|
use gpui::{px, Styled, WindowContext};
|
||||||
|
use settings::Settings;
|
||||||
|
use theme::ThemeSettings;
|
||||||
|
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::{ElevationIndex, UITextSize};
|
use crate::{ElevationIndex, UITextSize};
|
||||||
|
@ -60,6 +62,18 @@ pub trait StyledExt: Styled + Sized {
|
||||||
self.text_size(size)
|
self.text_size(size)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The font size for buffer text.
|
||||||
|
///
|
||||||
|
/// Retrieves the default font size, or the user's custom font size if set.
|
||||||
|
///
|
||||||
|
/// This should only be used for text that is displayed in a buffer,
|
||||||
|
/// or other places that text needs to match the user's buffer font size.
|
||||||
|
fn text_buffer(self, cx: &mut WindowContext) -> Self {
|
||||||
|
let settings = ThemeSettings::get_global(cx);
|
||||||
|
|
||||||
|
self.text_size(settings.buffer_font_size)
|
||||||
|
}
|
||||||
|
|
||||||
/// The [`Surface`](ui2::ElevationIndex::Surface) elevation level, located above the app background, is the standard level for all elements
|
/// The [`Surface`](ui2::ElevationIndex::Surface) elevation level, located above the app background, is the standard level for all elements
|
||||||
///
|
///
|
||||||
/// Sets `bg()`, `rounded_lg()`, `border()`, `border_color()`, `shadow()`
|
/// Sets `bg()`, `rounded_lg()`, `border()`, `border_color()`, `shadow()`
|
||||||
|
|
|
@ -7,8 +7,8 @@ use gpui::{
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use ui::prelude::*;
|
use ui::{h_stack, ContextMenu, IconButton, Tooltip};
|
||||||
use ui::{h_stack, menu_handle, ContextMenu, IconButton, Tooltip};
|
use ui::{prelude::*, right_click_menu};
|
||||||
|
|
||||||
pub enum PanelEvent {
|
pub enum PanelEvent {
|
||||||
ChangePosition,
|
ChangePosition,
|
||||||
|
@ -702,7 +702,7 @@ impl Render for PanelButtons {
|
||||||
};
|
};
|
||||||
|
|
||||||
Some(
|
Some(
|
||||||
menu_handle(name)
|
right_click_menu(name)
|
||||||
.menu(move |cx| {
|
.menu(move |cx| {
|
||||||
const POSITIONS: [DockPosition; 3] = [
|
const POSITIONS: [DockPosition; 3] = [
|
||||||
DockPosition::Left,
|
DockPosition::Left,
|
||||||
|
@ -726,14 +726,14 @@ impl Render for PanelButtons {
|
||||||
})
|
})
|
||||||
.anchor(menu_anchor)
|
.anchor(menu_anchor)
|
||||||
.attach(menu_attach)
|
.attach(menu_attach)
|
||||||
.child(move |_is_open| {
|
.trigger(
|
||||||
IconButton::new(name, icon)
|
IconButton::new(name, icon)
|
||||||
.selected(is_active_button)
|
.selected(is_active_button)
|
||||||
.action(action.boxed_clone())
|
.action(action.boxed_clone())
|
||||||
.tooltip(move |cx| {
|
.tooltip(move |cx| {
|
||||||
Tooltip::for_action(tooltip.clone(), &*action, cx)
|
Tooltip::for_action(tooltip.clone(), &*action, cx)
|
||||||
})
|
|
||||||
}),
|
}),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -135,24 +135,22 @@ impl Workspace {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn show_toast(&mut self, toast: Toast, cx: &mut ViewContext<Self>) {
|
pub fn show_toast(&mut self, toast: Toast, cx: &mut ViewContext<Self>) {
|
||||||
todo!()
|
self.dismiss_notification::<simple_message_notification::MessageNotification>(toast.id, cx);
|
||||||
// self.dismiss_notification::<simple_message_notification::MessageNotification>(toast.id, cx);
|
self.show_notification(toast.id, cx, |cx| {
|
||||||
// self.show_notification(toast.id, cx, |cx| {
|
cx.build_view(|_cx| match toast.on_click.as_ref() {
|
||||||
// cx.add_view(|_cx| match toast.on_click.as_ref() {
|
Some((click_msg, on_click)) => {
|
||||||
// Some((click_msg, on_click)) => {
|
let on_click = on_click.clone();
|
||||||
// let on_click = on_click.clone();
|
simple_message_notification::MessageNotification::new(toast.msg.clone())
|
||||||
// simple_message_notification::MessageNotification::new(toast.msg.clone())
|
.with_click_message(click_msg.clone())
|
||||||
// .with_click_message(click_msg.clone())
|
.on_click(move |cx| on_click(cx))
|
||||||
// .on_click(move |cx| on_click(cx))
|
}
|
||||||
// }
|
None => simple_message_notification::MessageNotification::new(toast.msg.clone()),
|
||||||
// None => simple_message_notification::MessageNotification::new(toast.msg.clone()),
|
})
|
||||||
// })
|
})
|
||||||
// })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn dismiss_toast(&mut self, id: usize, cx: &mut ViewContext<Self>) {
|
pub fn dismiss_toast(&mut self, id: usize, cx: &mut ViewContext<Self>) {
|
||||||
todo!()
|
self.dismiss_notification::<simple_message_notification::MessageNotification>(id, cx);
|
||||||
// self.dismiss_notification::<simple_message_notification::MessageNotification>(id, cx);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dismiss_notification_internal(
|
fn dismiss_notification_internal(
|
||||||
|
@ -179,33 +177,10 @@ pub mod simple_message_notification {
|
||||||
ParentElement, Render, SharedString, StatefulInteractiveElement, Styled, TextStyle,
|
ParentElement, Render, SharedString, StatefulInteractiveElement, Styled, TextStyle,
|
||||||
ViewContext,
|
ViewContext,
|
||||||
};
|
};
|
||||||
use serde::Deserialize;
|
use std::sync::Arc;
|
||||||
use std::{borrow::Cow, sync::Arc};
|
|
||||||
use ui::prelude::*;
|
use ui::prelude::*;
|
||||||
use ui::{h_stack, v_stack, Button, Icon, IconElement, Label, StyledExt};
|
use ui::{h_stack, v_stack, Button, Icon, IconElement, Label, StyledExt};
|
||||||
|
|
||||||
#[derive(Clone, Default, Deserialize, PartialEq)]
|
|
||||||
pub struct OsOpen(pub Cow<'static, str>);
|
|
||||||
|
|
||||||
impl OsOpen {
|
|
||||||
pub fn new<I: Into<Cow<'static, str>>>(url: I) -> Self {
|
|
||||||
OsOpen(url.into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// todo!()
|
|
||||||
// impl_actions!(message_notifications, [OsOpen]);
|
|
||||||
//
|
|
||||||
// todo!()
|
|
||||||
// pub fn init(cx: &mut AppContext) {
|
|
||||||
// cx.add_action(MessageNotification::dismiss);
|
|
||||||
// cx.add_action(
|
|
||||||
// |_workspace: &mut Workspace, open_action: &OsOpen, cx: &mut ViewContext<Workspace>| {
|
|
||||||
// cx.platform().open_url(open_action.0.as_ref());
|
|
||||||
// },
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
|
|
||||||
enum NotificationMessage {
|
enum NotificationMessage {
|
||||||
Text(SharedString),
|
Text(SharedString),
|
||||||
Element(fn(TextStyle, &AppContext) -> AnyElement),
|
Element(fn(TextStyle, &AppContext) -> AnyElement),
|
||||||
|
@ -213,7 +188,7 @@ pub mod simple_message_notification {
|
||||||
|
|
||||||
pub struct MessageNotification {
|
pub struct MessageNotification {
|
||||||
message: NotificationMessage,
|
message: NotificationMessage,
|
||||||
on_click: Option<Arc<dyn Fn(&mut ViewContext<Self>) + Send + Sync>>,
|
on_click: Option<Arc<dyn Fn(&mut ViewContext<Self>)>>,
|
||||||
click_message: Option<SharedString>,
|
click_message: Option<SharedString>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -252,7 +227,7 @@ pub mod simple_message_notification {
|
||||||
|
|
||||||
pub fn on_click<F>(mut self, on_click: F) -> Self
|
pub fn on_click<F>(mut self, on_click: F) -> Self
|
||||||
where
|
where
|
||||||
F: 'static + Send + Sync + Fn(&mut ViewContext<Self>),
|
F: 'static + Fn(&mut ViewContext<Self>),
|
||||||
{
|
{
|
||||||
self.on_click = Some(Arc::new(on_click));
|
self.on_click = Some(Arc::new(on_click));
|
||||||
self
|
self
|
||||||
|
|
|
@ -2,14 +2,15 @@ use crate::{
|
||||||
item::{Item, ItemHandle, ItemSettings, WeakItemHandle},
|
item::{Item, ItemHandle, ItemSettings, WeakItemHandle},
|
||||||
toolbar::Toolbar,
|
toolbar::Toolbar,
|
||||||
workspace_settings::{AutosaveSetting, WorkspaceSettings},
|
workspace_settings::{AutosaveSetting, WorkspaceSettings},
|
||||||
SplitDirection, Workspace,
|
NewCenterTerminal, NewFile, NewSearch, SplitDirection, ToggleZoom, Workspace,
|
||||||
};
|
};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use collections::{HashMap, HashSet, VecDeque};
|
use collections::{HashMap, HashSet, VecDeque};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, prelude::*, Action, AppContext, AsyncWindowContext, Div, EntityId, EventEmitter,
|
actions, overlay, prelude::*, rems, Action, AnchorCorner, AnyWeakView, AppContext,
|
||||||
FocusHandle, Focusable, FocusableView, Model, Pixels, Point, PromptLevel, Render, Task, View,
|
AsyncWindowContext, DismissEvent, Div, EntityId, EventEmitter, FocusHandle, Focusable,
|
||||||
ViewContext, VisualContext, WeakView, WindowContext,
|
FocusableView, Model, Pixels, Point, PromptLevel, Render, Task, View, ViewContext,
|
||||||
|
VisualContext, WeakView, WindowContext,
|
||||||
};
|
};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use project::{Project, ProjectEntryId, ProjectPath};
|
use project::{Project, ProjectEntryId, ProjectPath};
|
||||||
|
@ -25,8 +26,10 @@ use std::{
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
use ui::v_stack;
|
use ui::{
|
||||||
use ui::{prelude::*, Color, Icon, IconButton, IconElement, Tooltip};
|
h_stack, prelude::*, right_click_menu, Color, Icon, IconButton, IconElement, Label, Tooltip,
|
||||||
|
};
|
||||||
|
use ui::{v_stack, ContextMenu};
|
||||||
use util::truncate_and_remove_front;
|
use util::truncate_and_remove_front;
|
||||||
|
|
||||||
#[derive(PartialEq, Clone, Copy, Deserialize, Debug)]
|
#[derive(PartialEq, Clone, Copy, Deserialize, Debug)]
|
||||||
|
@ -50,7 +53,7 @@ pub enum SaveIntent {
|
||||||
|
|
||||||
//todo!("Do we need the default bound on actions? Decide soon")
|
//todo!("Do we need the default bound on actions? Decide soon")
|
||||||
// #[register_action]
|
// #[register_action]
|
||||||
#[derive(Clone, Deserialize, PartialEq, Debug)]
|
#[derive(Action, Clone, Deserialize, PartialEq, Debug)]
|
||||||
pub struct ActivateItem(pub usize);
|
pub struct ActivateItem(pub usize);
|
||||||
|
|
||||||
// #[derive(Clone, PartialEq)]
|
// #[derive(Clone, PartialEq)]
|
||||||
|
@ -143,17 +146,24 @@ impl fmt::Debug for Event {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct FocusedView {
|
||||||
|
view: AnyWeakView,
|
||||||
|
focus_handle: FocusHandle,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct Pane {
|
pub struct Pane {
|
||||||
focus_handle: FocusHandle,
|
focus_handle: FocusHandle,
|
||||||
items: Vec<Box<dyn ItemHandle>>,
|
items: Vec<Box<dyn ItemHandle>>,
|
||||||
activation_history: Vec<EntityId>,
|
activation_history: Vec<EntityId>,
|
||||||
zoomed: bool,
|
zoomed: bool,
|
||||||
active_item_index: usize,
|
active_item_index: usize,
|
||||||
// last_focused_view_by_item: HashMap<usize, AnyWeakViewHandle>,
|
last_focused_view_by_item: HashMap<EntityId, FocusHandle>,
|
||||||
autoscroll: bool,
|
autoscroll: bool,
|
||||||
nav_history: NavHistory,
|
nav_history: NavHistory,
|
||||||
toolbar: View<Toolbar>,
|
toolbar: View<Toolbar>,
|
||||||
// tab_bar_context_menu: TabBarContextMenu,
|
tab_bar_focus_handle: FocusHandle,
|
||||||
|
new_item_menu: Option<View<ContextMenu>>,
|
||||||
|
split_item_menu: Option<View<ContextMenu>>,
|
||||||
// tab_context_menu: ViewHandle<ContextMenu>,
|
// tab_context_menu: ViewHandle<ContextMenu>,
|
||||||
workspace: WeakView<Workspace>,
|
workspace: WeakView<Workspace>,
|
||||||
project: Model<Project>,
|
project: Model<Project>,
|
||||||
|
@ -306,7 +316,7 @@ impl Pane {
|
||||||
activation_history: Vec::new(),
|
activation_history: Vec::new(),
|
||||||
zoomed: false,
|
zoomed: false,
|
||||||
active_item_index: 0,
|
active_item_index: 0,
|
||||||
// last_focused_view_by_item: Default::default(),
|
last_focused_view_by_item: Default::default(),
|
||||||
autoscroll: false,
|
autoscroll: false,
|
||||||
nav_history: NavHistory(Arc::new(Mutex::new(NavHistoryState {
|
nav_history: NavHistory(Arc::new(Mutex::new(NavHistoryState {
|
||||||
mode: NavigationMode::Normal,
|
mode: NavigationMode::Normal,
|
||||||
|
@ -318,6 +328,9 @@ impl Pane {
|
||||||
next_timestamp,
|
next_timestamp,
|
||||||
}))),
|
}))),
|
||||||
toolbar: cx.build_view(|_| Toolbar::new()),
|
toolbar: cx.build_view(|_| Toolbar::new()),
|
||||||
|
tab_bar_focus_handle: cx.focus_handle(),
|
||||||
|
new_item_menu: None,
|
||||||
|
split_item_menu: None,
|
||||||
// tab_bar_context_menu: TabBarContextMenu {
|
// tab_bar_context_menu: TabBarContextMenu {
|
||||||
// kind: TabBarContextMenuKind::New,
|
// kind: TabBarContextMenuKind::New,
|
||||||
// handle: context_menu,
|
// handle: context_menu,
|
||||||
|
@ -392,9 +405,48 @@ impl Pane {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn has_focus(&self, cx: &WindowContext) -> bool {
|
pub fn has_focus(&self, cx: &WindowContext) -> bool {
|
||||||
|
// todo!(); // inline this manually
|
||||||
self.focus_handle.contains_focused(cx)
|
self.focus_handle.contains_focused(cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn focus_in(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
|
if !self.has_focus(cx) {
|
||||||
|
cx.emit(Event::Focus);
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.toolbar.update(cx, |toolbar, cx| {
|
||||||
|
toolbar.focus_changed(true, cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(active_item) = self.active_item() {
|
||||||
|
if self.focus_handle.is_focused(cx) {
|
||||||
|
// Pane was focused directly. We need to either focus a view inside the active item,
|
||||||
|
// or focus the active item itself
|
||||||
|
if let Some(weak_last_focused_view) =
|
||||||
|
self.last_focused_view_by_item.get(&active_item.item_id())
|
||||||
|
{
|
||||||
|
weak_last_focused_view.focus(cx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
active_item.focus_handle(cx).focus(cx);
|
||||||
|
} else if !self.tab_bar_focus_handle.contains_focused(cx) {
|
||||||
|
if let Some(focused) = cx.focused() {
|
||||||
|
self.last_focused_view_by_item
|
||||||
|
.insert(active_item.item_id(), focused);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn focus_out(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
|
self.toolbar.update(cx, |toolbar, cx| {
|
||||||
|
toolbar.focus_changed(false, cx);
|
||||||
|
});
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
pub fn active_item_index(&self) -> usize {
|
pub fn active_item_index(&self) -> usize {
|
||||||
self.active_item_index
|
self.active_item_index
|
||||||
}
|
}
|
||||||
|
@ -652,21 +704,16 @@ impl Pane {
|
||||||
.position(|i| i.item_id() == item.item_id())
|
.position(|i| i.item_id() == item.item_id())
|
||||||
}
|
}
|
||||||
|
|
||||||
// pub fn toggle_zoom(&mut self, _: &ToggleZoom, cx: &mut ViewContext<Self>) {
|
pub fn toggle_zoom(&mut self, _: &ToggleZoom, cx: &mut ViewContext<Self>) {
|
||||||
// // Potentially warn the user of the new keybinding
|
if self.zoomed {
|
||||||
// let workspace_handle = self.workspace().clone();
|
cx.emit(Event::ZoomOut);
|
||||||
// cx.spawn(|_, mut cx| async move { notify_of_new_dock(&workspace_handle, &mut cx) })
|
} else if !self.items.is_empty() {
|
||||||
// .detach();
|
if !self.focus_handle.contains_focused(cx) {
|
||||||
|
cx.focus_self();
|
||||||
// if self.zoomed {
|
}
|
||||||
// cx.emit(Event::ZoomOut);
|
cx.emit(Event::ZoomIn);
|
||||||
// } else if !self.items.is_empty() {
|
}
|
||||||
// if !self.has_focus {
|
}
|
||||||
// cx.focus_self();
|
|
||||||
// }
|
|
||||||
// cx.emit(Event::ZoomIn);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
pub fn activate_item(
|
pub fn activate_item(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
@ -1403,7 +1450,7 @@ impl Pane {
|
||||||
let close_right = ItemSettings::get_global(cx).close_position.right();
|
let close_right = ItemSettings::get_global(cx).close_position.right();
|
||||||
let is_active = ix == self.active_item_index;
|
let is_active = ix == self.active_item_index;
|
||||||
|
|
||||||
div()
|
let tab = div()
|
||||||
.group("")
|
.group("")
|
||||||
.id(ix)
|
.id(ix)
|
||||||
.cursor_pointer()
|
.cursor_pointer()
|
||||||
|
@ -1477,31 +1524,56 @@ impl Pane {
|
||||||
.children((!close_right).then(|| close_icon()))
|
.children((!close_right).then(|| close_icon()))
|
||||||
.child(label)
|
.child(label)
|
||||||
.children(close_right.then(|| close_icon())),
|
.children(close_right.then(|| close_icon())),
|
||||||
|
);
|
||||||
|
|
||||||
|
right_click_menu(ix).trigger(tab).menu(|cx| {
|
||||||
|
ContextMenu::build(cx, |menu, cx| {
|
||||||
|
menu.action(
|
||||||
|
"Close Active Item",
|
||||||
|
CloseActiveItem { save_intent: None }.boxed_clone(),
|
||||||
|
cx,
|
||||||
)
|
)
|
||||||
|
.action("Close Inactive Items", CloseInactiveItems.boxed_clone(), cx)
|
||||||
|
.action("Close Clean Items", CloseCleanItems.boxed_clone(), cx)
|
||||||
|
.action(
|
||||||
|
"Close Items To The Left",
|
||||||
|
CloseItemsToTheLeft.boxed_clone(),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.action(
|
||||||
|
"Close Items To The Right",
|
||||||
|
CloseItemsToTheRight.boxed_clone(),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.action(
|
||||||
|
"Close All Items",
|
||||||
|
CloseAllItems { save_intent: None }.boxed_clone(),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_tab_bar(&mut self, cx: &mut ViewContext<'_, Pane>) -> impl IntoElement {
|
fn render_tab_bar(&mut self, cx: &mut ViewContext<'_, Pane>) -> impl IntoElement {
|
||||||
div()
|
div()
|
||||||
.group("tab_bar")
|
|
||||||
.id("tab_bar")
|
.id("tab_bar")
|
||||||
|
.group("tab_bar")
|
||||||
|
.track_focus(&self.tab_bar_focus_handle)
|
||||||
.w_full()
|
.w_full()
|
||||||
|
// 30px @ 16px/rem
|
||||||
|
.h(rems(1.875))
|
||||||
|
.overflow_hidden()
|
||||||
.flex()
|
.flex()
|
||||||
|
.flex_none()
|
||||||
.bg(cx.theme().colors().tab_bar_background)
|
.bg(cx.theme().colors().tab_bar_background)
|
||||||
// Left Side
|
// Left Side
|
||||||
.child(
|
.child(
|
||||||
div()
|
h_stack()
|
||||||
.relative()
|
.px_2()
|
||||||
.px_1()
|
|
||||||
.flex()
|
.flex()
|
||||||
.flex_none()
|
.flex_none()
|
||||||
.gap_2()
|
.gap_1()
|
||||||
// Nav Buttons
|
// Nav Buttons
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.right_0()
|
|
||||||
.flex()
|
|
||||||
.items_center()
|
|
||||||
.gap_px()
|
|
||||||
.child(
|
.child(
|
||||||
div().border().border_color(gpui::red()).child(
|
div().border().border_color(gpui::red()).child(
|
||||||
IconButton::new("navigate_backward", Icon::ArrowLeft)
|
IconButton::new("navigate_backward", Icon::ArrowLeft)
|
||||||
|
@ -1522,7 +1594,6 @@ impl Pane {
|
||||||
.disabled(!self.can_navigate_forward()),
|
.disabled(!self.can_navigate_forward()),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div().flex_1().h_full().child(
|
div().flex_1().h_full().child(
|
||||||
|
@ -1550,20 +1621,87 @@ impl Pane {
|
||||||
.gap_px()
|
.gap_px()
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
|
.bg(gpui::blue())
|
||||||
.border()
|
.border()
|
||||||
.border_color(gpui::red())
|
.border_color(gpui::red())
|
||||||
.child(IconButton::new("plus", Icon::Plus)),
|
.child(IconButton::new("plus", Icon::Plus).on_click(
|
||||||
|
cx.listener(|this, _, cx| {
|
||||||
|
let menu = ContextMenu::build(cx, |menu, cx| {
|
||||||
|
menu.action("New File", NewFile.boxed_clone(), cx)
|
||||||
|
.action(
|
||||||
|
"New Terminal",
|
||||||
|
NewCenterTerminal.boxed_clone(),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.action(
|
||||||
|
"New Search",
|
||||||
|
NewSearch.boxed_clone(),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
cx.subscribe(
|
||||||
|
&menu,
|
||||||
|
|this, _, event: &DismissEvent, cx| {
|
||||||
|
this.focus(cx);
|
||||||
|
this.new_item_menu = None;
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.detach();
|
||||||
|
this.new_item_menu = Some(menu);
|
||||||
|
}),
|
||||||
|
))
|
||||||
|
.when_some(self.new_item_menu.as_ref(), |el, new_item_menu| {
|
||||||
|
el.child(Self::render_menu_overlay(new_item_menu))
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.border()
|
.border()
|
||||||
.border_color(gpui::red())
|
.border_color(gpui::red())
|
||||||
.child(IconButton::new("split", Icon::Split)),
|
.child(IconButton::new("split", Icon::Split).on_click(
|
||||||
|
cx.listener(|this, _, cx| {
|
||||||
|
let menu = ContextMenu::build(cx, |menu, cx| {
|
||||||
|
menu.action(
|
||||||
|
"Split Right",
|
||||||
|
SplitRight.boxed_clone(),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.action("Split Left", SplitLeft.boxed_clone(), cx)
|
||||||
|
.action("Split Up", SplitUp.boxed_clone(), cx)
|
||||||
|
.action("Split Down", SplitDown.boxed_clone(), cx)
|
||||||
|
});
|
||||||
|
cx.subscribe(
|
||||||
|
&menu,
|
||||||
|
|this, _, event: &DismissEvent, cx| {
|
||||||
|
this.focus(cx);
|
||||||
|
this.split_item_menu = None;
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.detach();
|
||||||
|
this.split_item_menu = Some(menu);
|
||||||
|
}),
|
||||||
|
))
|
||||||
|
.when_some(
|
||||||
|
self.split_item_menu.as_ref(),
|
||||||
|
|el, split_item_menu| {
|
||||||
|
el.child(Self::render_menu_overlay(split_item_menu))
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_menu_overlay(menu: &View<ContextMenu>) -> Div {
|
||||||
|
div()
|
||||||
|
.absolute()
|
||||||
|
.z_index(1)
|
||||||
|
.bottom_0()
|
||||||
|
.right_0()
|
||||||
|
.size_0()
|
||||||
|
.child(overlay().anchor(AnchorCorner::TopRight).child(menu.clone()))
|
||||||
|
}
|
||||||
|
|
||||||
// fn render_tabs(&mut self, cx: &mut ViewContext<Self>) -> impl Element<Self> {
|
// fn render_tabs(&mut self, cx: &mut ViewContext<Self>) -> impl Element<Self> {
|
||||||
// let theme = theme::current(cx).clone();
|
// let theme = theme::current(cx).clone();
|
||||||
|
|
||||||
|
@ -1962,9 +2100,23 @@ impl Render for Pane {
|
||||||
type Element = Focusable<Div>;
|
type Element = Focusable<Div>;
|
||||||
|
|
||||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||||
|
let this = cx.view().downgrade();
|
||||||
|
|
||||||
v_stack()
|
v_stack()
|
||||||
.key_context("Pane")
|
.key_context("Pane")
|
||||||
.track_focus(&self.focus_handle)
|
.track_focus(&self.focus_handle)
|
||||||
|
.on_focus_in({
|
||||||
|
let this = this.clone();
|
||||||
|
move |event, cx| {
|
||||||
|
this.update(cx, |this, cx| this.focus_in(cx)).ok();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on_focus_out({
|
||||||
|
let this = this.clone();
|
||||||
|
move |event, cx| {
|
||||||
|
this.update(cx, |this, cx| this.focus_out(cx)).ok();
|
||||||
|
}
|
||||||
|
})
|
||||||
.on_action(cx.listener(|pane, _: &SplitLeft, cx| pane.split(SplitDirection::Left, cx)))
|
.on_action(cx.listener(|pane, _: &SplitLeft, cx| pane.split(SplitDirection::Left, cx)))
|
||||||
.on_action(cx.listener(|pane, _: &SplitUp, cx| pane.split(SplitDirection::Up, cx)))
|
.on_action(cx.listener(|pane, _: &SplitUp, cx| pane.split(SplitDirection::Up, cx)))
|
||||||
.on_action(
|
.on_action(
|
||||||
|
@ -1973,25 +2125,53 @@ impl Render for Pane {
|
||||||
.on_action(cx.listener(|pane, _: &SplitDown, cx| pane.split(SplitDirection::Down, cx)))
|
.on_action(cx.listener(|pane, _: &SplitDown, cx| pane.split(SplitDirection::Down, cx)))
|
||||||
.on_action(cx.listener(|pane, _: &GoBack, cx| pane.navigate_backward(cx)))
|
.on_action(cx.listener(|pane, _: &GoBack, cx| pane.navigate_backward(cx)))
|
||||||
.on_action(cx.listener(|pane, _: &GoForward, cx| pane.navigate_forward(cx)))
|
.on_action(cx.listener(|pane, _: &GoForward, cx| pane.navigate_forward(cx)))
|
||||||
// cx.add_action(Pane::toggle_zoom);
|
.on_action(cx.listener(Pane::toggle_zoom))
|
||||||
// cx.add_action(|pane: &mut Pane, action: &ActivateItem, cx| {
|
.on_action(cx.listener(|pane: &mut Pane, action: &ActivateItem, cx| {
|
||||||
// pane.activate_item(action.0, true, true, cx);
|
pane.activate_item(action.0, true, true, cx);
|
||||||
// });
|
}))
|
||||||
// cx.add_action(|pane: &mut Pane, _: &ActivateLastItem, cx| {
|
.on_action(cx.listener(|pane: &mut Pane, _: &ActivateLastItem, cx| {
|
||||||
// pane.activate_item(pane.items.len() - 1, true, true, cx);
|
pane.activate_item(pane.items.len() - 1, true, true, cx);
|
||||||
// });
|
}))
|
||||||
// cx.add_action(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
|
.on_action(cx.listener(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
|
||||||
// pane.activate_prev_item(true, cx);
|
pane.activate_prev_item(true, cx);
|
||||||
// });
|
}))
|
||||||
// cx.add_action(|pane: &mut Pane, _: &ActivateNextItem, cx| {
|
.on_action(cx.listener(|pane: &mut Pane, _: &ActivateNextItem, cx| {
|
||||||
// pane.activate_next_item(true, cx);
|
pane.activate_next_item(true, cx);
|
||||||
// });
|
}))
|
||||||
// cx.add_async_action(Pane::close_active_item);
|
.on_action(
|
||||||
// cx.add_async_action(Pane::close_inactive_items);
|
cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| {
|
||||||
// cx.add_async_action(Pane::close_clean_items);
|
pane.close_active_item(action, cx)
|
||||||
// cx.add_async_action(Pane::close_items_to_the_left);
|
.map(|task| task.detach_and_log_err(cx));
|
||||||
// cx.add_async_action(Pane::close_items_to_the_right);
|
}),
|
||||||
// cx.add_async_action(Pane::close_all_items);
|
)
|
||||||
|
.on_action(
|
||||||
|
cx.listener(|pane: &mut Self, action: &CloseInactiveItems, cx| {
|
||||||
|
pane.close_inactive_items(action, cx)
|
||||||
|
.map(|task| task.detach_and_log_err(cx));
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.on_action(
|
||||||
|
cx.listener(|pane: &mut Self, action: &CloseCleanItems, cx| {
|
||||||
|
pane.close_clean_items(action, cx)
|
||||||
|
.map(|task| task.detach_and_log_err(cx));
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.on_action(
|
||||||
|
cx.listener(|pane: &mut Self, action: &CloseItemsToTheLeft, cx| {
|
||||||
|
pane.close_items_to_the_left(action, cx)
|
||||||
|
.map(|task| task.detach_and_log_err(cx));
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.on_action(
|
||||||
|
cx.listener(|pane: &mut Self, action: &CloseItemsToTheRight, cx| {
|
||||||
|
pane.close_items_to_the_right(action, cx)
|
||||||
|
.map(|task| task.detach_and_log_err(cx));
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.on_action(cx.listener(|pane: &mut Self, action: &CloseAllItems, cx| {
|
||||||
|
pane.close_all_items(action, cx)
|
||||||
|
.map(|task| task.detach_and_log_err(cx));
|
||||||
|
}))
|
||||||
.size_full()
|
.size_full()
|
||||||
.on_action(
|
.on_action(
|
||||||
cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| {
|
cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| {
|
||||||
|
@ -2004,8 +2184,11 @@ impl Render for Pane {
|
||||||
.child(if let Some(item) = self.active_item() {
|
.child(if let Some(item) = self.active_item() {
|
||||||
div().flex().flex_1().child(item.to_any())
|
div().flex().flex_1().child(item.to_any())
|
||||||
} else {
|
} else {
|
||||||
// todo!()
|
h_stack()
|
||||||
div().child("Empty Pane")
|
.items_center()
|
||||||
|
.size_full()
|
||||||
|
.justify_center()
|
||||||
|
.child(Label::new("Open a file or project to get started.").color(Color::Muted))
|
||||||
})
|
})
|
||||||
|
|
||||||
// enum MouseNavigationHandler {}
|
// enum MouseNavigationHandler {}
|
||||||
|
|
|
@ -6,7 +6,7 @@ use gpui::{
|
||||||
WindowContext,
|
WindowContext,
|
||||||
};
|
};
|
||||||
use ui::prelude::*;
|
use ui::prelude::*;
|
||||||
use ui::{h_stack, Button, Icon, IconButton};
|
use ui::{h_stack, Icon, IconButton};
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
|
|
||||||
pub trait StatusItemView: Render {
|
pub trait StatusItemView: Render {
|
||||||
|
@ -53,24 +53,6 @@ impl Render for StatusBar {
|
||||||
.gap_4()
|
.gap_4()
|
||||||
.child(
|
.child(
|
||||||
h_stack().gap_1().child(
|
h_stack().gap_1().child(
|
||||||
// TODO: Language picker
|
|
||||||
div()
|
|
||||||
.border()
|
|
||||||
.border_color(gpui::red())
|
|
||||||
.child(Button::new("status_buffer_language", "Rust")),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
h_stack()
|
|
||||||
.gap_1()
|
|
||||||
.child(
|
|
||||||
// Github tool
|
|
||||||
div()
|
|
||||||
.border()
|
|
||||||
.border_color(gpui::red())
|
|
||||||
.child(IconButton::new("status-copilot", Icon::Copilot)),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
// Feedback Tool
|
// Feedback Tool
|
||||||
div()
|
div()
|
||||||
.border()
|
.border()
|
||||||
|
@ -78,16 +60,6 @@ impl Render for StatusBar {
|
||||||
.child(IconButton::new("status-feedback", Icon::Envelope)),
|
.child(IconButton::new("status-feedback", Icon::Envelope)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.child(
|
|
||||||
// Bottom Dock
|
|
||||||
h_stack().gap_1().child(
|
|
||||||
// Terminal
|
|
||||||
div()
|
|
||||||
.border()
|
|
||||||
.border_color(gpui::red())
|
|
||||||
.child(IconButton::new("status-terminal", Icon::Terminal)),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.child(
|
.child(
|
||||||
// Right Dock
|
// Right Dock
|
||||||
h_stack()
|
h_stack()
|
||||||
|
|
|
@ -4,7 +4,7 @@ use gpui::{
|
||||||
ViewContext, WindowContext,
|
ViewContext, WindowContext,
|
||||||
};
|
};
|
||||||
use ui::prelude::*;
|
use ui::prelude::*;
|
||||||
use ui::{h_stack, v_stack, ButtonLike, Color, Icon, IconButton, Label};
|
use ui::{h_stack, v_stack, Icon, IconButton};
|
||||||
|
|
||||||
pub enum ToolbarItemEvent {
|
pub enum ToolbarItemEvent {
|
||||||
ChangeLocation(ToolbarItemLocation),
|
ChangeLocation(ToolbarItemLocation),
|
||||||
|
@ -87,17 +87,8 @@ impl Render for Toolbar {
|
||||||
.child(
|
.child(
|
||||||
h_stack()
|
h_stack()
|
||||||
.justify_between()
|
.justify_between()
|
||||||
.child(
|
|
||||||
// Toolbar left side
|
// Toolbar left side
|
||||||
h_stack().border().border_color(gpui::red()).p_1().child(
|
.children(self.items.iter().map(|(child, _)| child.to_any()))
|
||||||
ButtonLike::new("breadcrumb")
|
|
||||||
.child(Label::new("crates/workspace2/src/toolbar.rs"))
|
|
||||||
.child(Label::new("›").color(Color::Muted))
|
|
||||||
.child(Label::new("impl Render for Toolbar"))
|
|
||||||
.child(Label::new("›").color(Color::Muted))
|
|
||||||
.child(Label::new("fn render")),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
// Toolbar right side
|
// Toolbar right side
|
||||||
.child(
|
.child(
|
||||||
h_stack()
|
h_stack()
|
||||||
|
@ -116,7 +107,6 @@ impl Render for Toolbar {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.children(self.items.iter().map(|(child, _)| child.to_any()))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3585,87 +3585,6 @@ fn open_items(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// todo!()
|
|
||||||
// fn notify_of_new_dock(workspace: &WeakView<Workspace>, cx: &mut AsyncAppContext) {
|
|
||||||
// const NEW_PANEL_BLOG_POST: &str = "https://zed.dev/blog/new-panel-system";
|
|
||||||
// const NEW_DOCK_HINT_KEY: &str = "show_new_dock_key";
|
|
||||||
// const MESSAGE_ID: usize = 2;
|
|
||||||
|
|
||||||
// if workspace
|
|
||||||
// .read_with(cx, |workspace, cx| {
|
|
||||||
// workspace.has_shown_notification_once::<MessageNotification>(MESSAGE_ID, cx)
|
|
||||||
// })
|
|
||||||
// .unwrap_or(false)
|
|
||||||
// {
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if db::kvp::KEY_VALUE_STORE
|
|
||||||
// .read_kvp(NEW_DOCK_HINT_KEY)
|
|
||||||
// .ok()
|
|
||||||
// .flatten()
|
|
||||||
// .is_some()
|
|
||||||
// {
|
|
||||||
// if !workspace
|
|
||||||
// .read_with(cx, |workspace, cx| {
|
|
||||||
// workspace.has_shown_notification_once::<MessageNotification>(MESSAGE_ID, cx)
|
|
||||||
// })
|
|
||||||
// .unwrap_or(false)
|
|
||||||
// {
|
|
||||||
// cx.update(|cx| {
|
|
||||||
// cx.update_global::<NotificationTracker, _, _>(|tracker, _| {
|
|
||||||
// let entry = tracker
|
|
||||||
// .entry(TypeId::of::<MessageNotification>())
|
|
||||||
// .or_default();
|
|
||||||
// if !entry.contains(&MESSAGE_ID) {
|
|
||||||
// entry.push(MESSAGE_ID);
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// cx.spawn(|_| async move {
|
|
||||||
// db::kvp::KEY_VALUE_STORE
|
|
||||||
// .write_kvp(NEW_DOCK_HINT_KEY.to_string(), "seen".to_string())
|
|
||||||
// .await
|
|
||||||
// .ok();
|
|
||||||
// })
|
|
||||||
// .detach();
|
|
||||||
|
|
||||||
// workspace
|
|
||||||
// .update(cx, |workspace, cx| {
|
|
||||||
// workspace.show_notification_once(2, cx, |cx| {
|
|
||||||
// cx.build_view(|_| {
|
|
||||||
// MessageNotification::new_element(|text, _| {
|
|
||||||
// Text::new(
|
|
||||||
// "Looking for the dock? Try ctrl-`!\nshift-escape now zooms your pane.",
|
|
||||||
// text,
|
|
||||||
// )
|
|
||||||
// .with_custom_runs(vec![26..32, 34..46], |_, bounds, cx| {
|
|
||||||
// let code_span_background_color = settings::get::<ThemeSettings>(cx)
|
|
||||||
// .theme
|
|
||||||
// .editor
|
|
||||||
// .document_highlight_read_background;
|
|
||||||
|
|
||||||
// cx.scene().push_quad(gpui::Quad {
|
|
||||||
// bounds,
|
|
||||||
// background: Some(code_span_background_color),
|
|
||||||
// border: Default::default(),
|
|
||||||
// corner_radii: (2.0).into(),
|
|
||||||
// })
|
|
||||||
// })
|
|
||||||
// .into_any()
|
|
||||||
// })
|
|
||||||
// .with_click_message("Read more about the new panel system")
|
|
||||||
// .on_click(|cx| cx.platform().open_url(NEW_PANEL_BLOG_POST))
|
|
||||||
// })
|
|
||||||
// })
|
|
||||||
// })
|
|
||||||
// .ok();
|
|
||||||
|
|
||||||
fn notify_if_database_failed(workspace: WindowHandle<Workspace>, cx: &mut AsyncAppContext) {
|
fn notify_if_database_failed(workspace: WindowHandle<Workspace>, cx: &mut AsyncAppContext) {
|
||||||
const REPORT_ISSUE_URL: &str ="https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml";
|
const REPORT_ISSUE_URL: &str ="https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml";
|
||||||
|
|
||||||
|
@ -3719,6 +3638,8 @@ impl Render for Workspace {
|
||||||
.items_start()
|
.items_start()
|
||||||
.text_color(cx.theme().colors().text)
|
.text_color(cx.theme().colors().text)
|
||||||
.bg(cx.theme().colors().background)
|
.bg(cx.theme().colors().background)
|
||||||
|
.border()
|
||||||
|
.border_color(cx.theme().colors().border)
|
||||||
.children(self.titlebar_item.clone())
|
.children(self.titlebar_item.clone())
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
|
|
|
@ -172,7 +172,7 @@ osx_info_plist_exts = ["resources/info/*"]
|
||||||
osx_url_schemes = ["zed-dev"]
|
osx_url_schemes = ["zed-dev"]
|
||||||
|
|
||||||
[package.metadata.bundle-nightly]
|
[package.metadata.bundle-nightly]
|
||||||
icon = ["resources/app-icon-preview@2x.png", "resources/app-icon-preview.png"]
|
icon = ["resources/app-icon-nightly@2x.png", "resources/app-icon-nightly.png"]
|
||||||
identifier = "dev.zed.Zed-Nightly"
|
identifier = "dev.zed.Zed-Nightly"
|
||||||
name = "Zed Nightly"
|
name = "Zed Nightly"
|
||||||
osx_minimum_system_version = "10.15.7"
|
osx_minimum_system_version = "10.15.7"
|
||||||
|
|
BIN
crates/zed/resources/app-icon-nightly.png
Normal file
BIN
crates/zed/resources/app-icon-nightly.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 187 KiB |
BIN
crates/zed/resources/app-icon-nightly@2x.png
Normal file
BIN
crates/zed/resources/app-icon-nightly@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 539 KiB |
|
@ -39,7 +39,7 @@ pub enum IsOnlyInstance {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ensure_only_instance() -> IsOnlyInstance {
|
pub fn ensure_only_instance() -> IsOnlyInstance {
|
||||||
if *db::ZED_STATELESS {
|
if *db::ZED_STATELESS || *util::channel::RELEASE_CHANNEL == ReleaseChannel::Dev {
|
||||||
return IsOnlyInstance::Yes;
|
return IsOnlyInstance::Yes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,7 @@ ai = { package = "ai2", path = "../ai2"}
|
||||||
audio = { package = "audio2", path = "../audio2" }
|
audio = { package = "audio2", path = "../audio2" }
|
||||||
activity_indicator = { package = "activity_indicator2", path = "../activity_indicator2"}
|
activity_indicator = { package = "activity_indicator2", path = "../activity_indicator2"}
|
||||||
auto_update = { package = "auto_update2", path = "../auto_update2" }
|
auto_update = { package = "auto_update2", path = "../auto_update2" }
|
||||||
# breadcrumbs = { path = "../breadcrumbs" }
|
breadcrumbs = { package = "breadcrumbs2", path = "../breadcrumbs2" }
|
||||||
call = { package = "call2", path = "../call2" }
|
call = { package = "call2", path = "../call2" }
|
||||||
channel = { package = "channel2", path = "../channel2" }
|
channel = { package = "channel2", path = "../channel2" }
|
||||||
cli = { path = "../cli" }
|
cli = { path = "../cli" }
|
||||||
|
@ -30,7 +30,7 @@ command_palette = { package="command_palette2", path = "../command_palette2" }
|
||||||
client = { package = "client2", path = "../client2" }
|
client = { package = "client2", path = "../client2" }
|
||||||
# clock = { path = "../clock" }
|
# clock = { path = "../clock" }
|
||||||
copilot = { package = "copilot2", path = "../copilot2" }
|
copilot = { package = "copilot2", path = "../copilot2" }
|
||||||
# copilot_button = { path = "../copilot_button" }
|
copilot_button = { package = "copilot_button2", path = "../copilot_button2" }
|
||||||
diagnostics = { package = "diagnostics2", path = "../diagnostics2" }
|
diagnostics = { package = "diagnostics2", path = "../diagnostics2" }
|
||||||
db = { package = "db2", path = "../db2" }
|
db = { package = "db2", path = "../db2" }
|
||||||
editor = { package="editor2", path = "../editor2" }
|
editor = { package="editor2", path = "../editor2" }
|
||||||
|
@ -44,7 +44,7 @@ gpui = { package = "gpui2", path = "../gpui2" }
|
||||||
install_cli = { package = "install_cli2", path = "../install_cli2" }
|
install_cli = { package = "install_cli2", path = "../install_cli2" }
|
||||||
journal = { package = "journal2", path = "../journal2" }
|
journal = { package = "journal2", path = "../journal2" }
|
||||||
language = { package = "language2", path = "../language2" }
|
language = { package = "language2", path = "../language2" }
|
||||||
# language_selector = { path = "../language_selector" }
|
language_selector = { package = "language_selector2", path = "../language_selector2" }
|
||||||
lsp = { package = "lsp2", path = "../lsp2" }
|
lsp = { package = "lsp2", path = "../lsp2" }
|
||||||
menu = { package = "menu2", path = "../menu2" }
|
menu = { package = "menu2", path = "../menu2" }
|
||||||
# language_tools = { path = "../language_tools" }
|
# language_tools = { path = "../language_tools" }
|
||||||
|
@ -167,7 +167,7 @@ osx_info_plist_exts = ["resources/info/*"]
|
||||||
osx_url_schemes = ["zed-dev"]
|
osx_url_schemes = ["zed-dev"]
|
||||||
|
|
||||||
[package.metadata.bundle-nightly]
|
[package.metadata.bundle-nightly]
|
||||||
icon = ["resources/app-icon-preview@2x.png", "resources/app-icon-preview.png"]
|
icon = ["resources/app-icon-nightly@2x.png", "resources/app-icon-nightly.png"]
|
||||||
identifier = "dev.zed.Zed-Dev"
|
identifier = "dev.zed.Zed-Dev"
|
||||||
name = "Zed Nightly"
|
name = "Zed Nightly"
|
||||||
osx_minimum_system_version = "10.15.7"
|
osx_minimum_system_version = "10.15.7"
|
||||||
|
|
BIN
crates/zed2/resources/app-icon-nightly.png
Normal file
BIN
crates/zed2/resources/app-icon-nightly.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 187 KiB |
BIN
crates/zed2/resources/app-icon-nightly@2x.png
Normal file
BIN
crates/zed2/resources/app-icon-nightly@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 539 KiB |
|
@ -216,7 +216,7 @@ fn main() {
|
||||||
terminal_view::init(cx);
|
terminal_view::init(cx);
|
||||||
|
|
||||||
// journal2::init(app_state.clone(), cx);
|
// journal2::init(app_state.clone(), cx);
|
||||||
// language_selector::init(cx);
|
language_selector::init(cx);
|
||||||
theme_selector::init(cx);
|
theme_selector::init(cx);
|
||||||
// activity_indicator::init(cx);
|
// activity_indicator::init(cx);
|
||||||
// language_tools::init(cx);
|
// language_tools::init(cx);
|
||||||
|
|
|
@ -39,7 +39,7 @@ pub enum IsOnlyInstance {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ensure_only_instance() -> IsOnlyInstance {
|
pub fn ensure_only_instance() -> IsOnlyInstance {
|
||||||
if *db::ZED_STATELESS {
|
if *db::ZED_STATELESS || *util::channel::RELEASE_CHANNEL == ReleaseChannel::Dev {
|
||||||
return IsOnlyInstance::Yes;
|
return IsOnlyInstance::Yes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ mod only_instance;
|
||||||
mod open_listener;
|
mod open_listener;
|
||||||
|
|
||||||
pub use assets::*;
|
pub use assets::*;
|
||||||
|
use breadcrumbs::Breadcrumbs;
|
||||||
use collections::VecDeque;
|
use collections::VecDeque;
|
||||||
use editor::{Editor, MultiBuffer};
|
use editor::{Editor, MultiBuffer};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
|
@ -95,11 +96,11 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
|
||||||
if let workspace::Event::PaneAdded(pane) = event {
|
if let workspace::Event::PaneAdded(pane) = event {
|
||||||
pane.update(cx, |pane, cx| {
|
pane.update(cx, |pane, cx| {
|
||||||
pane.toolbar().update(cx, |toolbar, cx| {
|
pane.toolbar().update(cx, |toolbar, cx| {
|
||||||
// todo!()
|
let breadcrumbs = cx.build_view(|_| Breadcrumbs::new(workspace));
|
||||||
// let breadcrumbs = cx.add_view(|_| Breadcrumbs::new(workspace));
|
toolbar.add_item(breadcrumbs, cx);
|
||||||
// toolbar.add_item(breadcrumbs, cx);
|
|
||||||
let buffer_search_bar = cx.build_view(search::BufferSearchBar::new);
|
let buffer_search_bar = cx.build_view(search::BufferSearchBar::new);
|
||||||
toolbar.add_item(buffer_search_bar.clone(), cx);
|
toolbar.add_item(buffer_search_bar.clone(), cx);
|
||||||
|
// todo!()
|
||||||
// let quick_action_bar = cx.add_view(|_| {
|
// let quick_action_bar = cx.add_view(|_| {
|
||||||
// QuickActionBar::new(buffer_search_bar, workspace)
|
// QuickActionBar::new(buffer_search_bar, workspace)
|
||||||
// });
|
// });
|
||||||
|
@ -135,14 +136,14 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
|
||||||
// cx.add_view(|cx| CollabTitlebarItem::new(workspace, &workspace_handle, cx));
|
// cx.add_view(|cx| CollabTitlebarItem::new(workspace, &workspace_handle, cx));
|
||||||
// workspace.set_titlebar_item(collab_titlebar_item.into_any(), cx);
|
// workspace.set_titlebar_item(collab_titlebar_item.into_any(), cx);
|
||||||
|
|
||||||
// let copilot =
|
let copilot =
|
||||||
// cx.add_view(|cx| copilot_button::CopilotButton::new(app_state.fs.clone(), cx));
|
cx.build_view(|cx| copilot_button::CopilotButton::new(app_state.fs.clone(), cx));
|
||||||
let diagnostic_summary =
|
let diagnostic_summary =
|
||||||
cx.build_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace, cx));
|
cx.build_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace, cx));
|
||||||
let activity_indicator =
|
let activity_indicator =
|
||||||
activity_indicator::ActivityIndicator::new(workspace, app_state.languages.clone(), cx);
|
activity_indicator::ActivityIndicator::new(workspace, app_state.languages.clone(), cx);
|
||||||
// let active_buffer_language =
|
let active_buffer_language =
|
||||||
// cx.add_view(|_| language_selector::ActiveBufferLanguage::new(workspace));
|
cx.build_view(|_| language_selector::ActiveBufferLanguage::new(workspace));
|
||||||
// let vim_mode_indicator = cx.add_view(|cx| vim::ModeIndicator::new(cx));
|
// let vim_mode_indicator = cx.add_view(|cx| vim::ModeIndicator::new(cx));
|
||||||
// let feedback_button = cx.add_view(|_| {
|
// let feedback_button = cx.add_view(|_| {
|
||||||
// feedback::deploy_feedback_button::DeployFeedbackButton::new(workspace)
|
// feedback::deploy_feedback_button::DeployFeedbackButton::new(workspace)
|
||||||
|
@ -153,8 +154,8 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
|
||||||
status_bar.add_left_item(activity_indicator, cx);
|
status_bar.add_left_item(activity_indicator, cx);
|
||||||
|
|
||||||
// status_bar.add_right_item(feedback_button, cx);
|
// status_bar.add_right_item(feedback_button, cx);
|
||||||
// status_bar.add_right_item(copilot, cx);
|
status_bar.add_right_item(copilot, cx);
|
||||||
// status_bar.add_right_item(active_buffer_language, cx);
|
status_bar.add_right_item(active_buffer_language, cx);
|
||||||
// status_bar.add_right_item(vim_mode_indicator, cx);
|
// status_bar.add_right_item(vim_mode_indicator, cx);
|
||||||
status_bar.add_right_item(cursor_position, cx);
|
status_bar.add_right_item(cursor_position, cx);
|
||||||
});
|
});
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue