mirror of
https://github.com/zed-industries/zed.git
synced 2024-11-08 07:35:01 +03:00
Merge branch 'main' into fix-splits-bugs
This commit is contained in:
commit
d88fc27b79
25
Cargo.lock
generated
25
Cargo.lock
generated
@ -6510,6 +6510,7 @@ dependencies = [
|
|||||||
"theme2",
|
"theme2",
|
||||||
"ui2",
|
"ui2",
|
||||||
"util",
|
"util",
|
||||||
|
"workspace2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -7019,6 +7020,29 @@ dependencies = [
|
|||||||
"workspace",
|
"workspace",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "project_symbols2"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"editor2",
|
||||||
|
"futures 0.3.28",
|
||||||
|
"fuzzy2",
|
||||||
|
"gpui2",
|
||||||
|
"language2",
|
||||||
|
"lsp2",
|
||||||
|
"ordered-float 2.10.0",
|
||||||
|
"picker2",
|
||||||
|
"postage",
|
||||||
|
"project2",
|
||||||
|
"settings2",
|
||||||
|
"smol",
|
||||||
|
"text2",
|
||||||
|
"theme2",
|
||||||
|
"util",
|
||||||
|
"workspace2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "prometheus"
|
name = "prometheus"
|
||||||
version = "0.13.3"
|
version = "0.13.3"
|
||||||
@ -12081,6 +12105,7 @@ dependencies = [
|
|||||||
"postage",
|
"postage",
|
||||||
"project2",
|
"project2",
|
||||||
"project_panel2",
|
"project_panel2",
|
||||||
|
"project_symbols2",
|
||||||
"quick_action_bar2",
|
"quick_action_bar2",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"recent_projects2",
|
"recent_projects2",
|
||||||
|
@ -90,6 +90,7 @@ members = [
|
|||||||
"crates/project_panel",
|
"crates/project_panel",
|
||||||
"crates/project_panel2",
|
"crates/project_panel2",
|
||||||
"crates/project_symbols",
|
"crates/project_symbols",
|
||||||
|
"crates/project_symbols2",
|
||||||
"crates/quick_action_bar2",
|
"crates/quick_action_bar2",
|
||||||
"crates/recent_projects",
|
"crates/recent_projects",
|
||||||
"crates/recent_projects2",
|
"crates/recent_projects2",
|
||||||
|
@ -21,10 +21,7 @@ use settings::{Settings, SettingsStore};
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use theme::ActiveTheme as _;
|
use theme::ActiveTheme as _;
|
||||||
use time::{OffsetDateTime, UtcOffset};
|
use time::{OffsetDateTime, UtcOffset};
|
||||||
use ui::{
|
use ui::{prelude::*, Avatar, Button, Icon, IconButton, Label, Tooltip};
|
||||||
h_stack, prelude::WindowContext, v_stack, Avatar, Button, ButtonCommon as _, Clickable, Icon,
|
|
||||||
IconButton, Label, Tooltip,
|
|
||||||
};
|
|
||||||
use util::{ResultExt, TryFutureExt};
|
use util::{ResultExt, TryFutureExt};
|
||||||
use workspace::{
|
use workspace::{
|
||||||
dock::{DockPosition, Panel, PanelEvent},
|
dock::{DockPosition, Panel, PanelEvent},
|
||||||
@ -382,13 +379,18 @@ impl ChatPanel {
|
|||||||
.child(text.element("body".into(), cx))
|
.child(text.element("body".into(), cx))
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.invisible()
|
|
||||||
.absolute()
|
.absolute()
|
||||||
.top_1()
|
.top_1()
|
||||||
.right_2()
|
.right_2()
|
||||||
.w_8()
|
.w_8()
|
||||||
.group_hover("", |this| this.visible())
|
.visible_on_hover("")
|
||||||
.child(render_remove(message_id_to_remove, cx)),
|
.children(message_id_to_remove.map(|message_id| {
|
||||||
|
IconButton::new(("remove", message_id), Icon::XCircle).on_click(
|
||||||
|
cx.listener(move |this, _, cx| {
|
||||||
|
this.remove_message(message_id, cx);
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})),
|
||||||
)
|
)
|
||||||
.into_any()
|
.into_any()
|
||||||
}
|
}
|
||||||
@ -528,18 +530,6 @@ impl ChatPanel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_remove(message_id_to_remove: Option<u64>, cx: &mut ViewContext<ChatPanel>) -> AnyElement {
|
|
||||||
if let Some(message_id) = message_id_to_remove {
|
|
||||||
IconButton::new(("remove", message_id), Icon::XCircle)
|
|
||||||
.on_click(cx.listener(move |this, _, cx| {
|
|
||||||
this.remove_message(message_id, cx);
|
|
||||||
}))
|
|
||||||
.into_any_element()
|
|
||||||
} else {
|
|
||||||
div().into_any_element()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl EventEmitter<Event> for ChatPanel {}
|
impl EventEmitter<Event> for ChatPanel {}
|
||||||
|
|
||||||
impl Render for ChatPanel {
|
impl Render for ChatPanel {
|
||||||
|
@ -176,11 +176,11 @@ use feature_flags::{ChannelsAlpha, FeatureFlagAppExt, FeatureFlagViewExt};
|
|||||||
use fuzzy::{match_strings, StringMatchCandidate};
|
use fuzzy::{match_strings, StringMatchCandidate};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, canvas, div, fill, img, impl_actions, overlay, point, prelude::*, px, rems,
|
actions, canvas, div, fill, img, impl_actions, overlay, point, prelude::*, px, rems,
|
||||||
serde_json, size, Action, AppContext, AsyncWindowContext, Bounds, ClipboardItem, DismissEvent,
|
serde_json, size, Action, AnyElement, AppContext, AsyncWindowContext, Bounds, ClipboardItem,
|
||||||
Div, EventEmitter, FocusHandle, Focusable, FocusableView, Hsla, InteractiveElement,
|
DismissEvent, Div, EventEmitter, FocusHandle, Focusable, FocusableView, Hsla,
|
||||||
IntoElement, Length, Model, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Quad,
|
InteractiveElement, IntoElement, Length, Model, MouseDownEvent, ParentElement, Pixels, Point,
|
||||||
Render, RenderOnce, ScrollHandle, SharedString, Size, Stateful, Styled, Subscription, Task,
|
PromptLevel, Quad, Render, RenderOnce, ScrollHandle, SharedString, Size, Stateful, Styled,
|
||||||
View, ViewContext, VisualContext, WeakView,
|
Subscription, Task, View, ViewContext, VisualContext, WeakView,
|
||||||
};
|
};
|
||||||
use project::{Fs, Project};
|
use project::{Fs, Project};
|
||||||
use serde_derive::{Deserialize, Serialize};
|
use serde_derive::{Deserialize, Serialize};
|
||||||
@ -402,7 +402,7 @@ impl CollabPanel {
|
|||||||
|
|
||||||
let filter_editor = cx.build_view(|cx| {
|
let filter_editor = cx.build_view(|cx| {
|
||||||
let mut editor = Editor::single_line(cx);
|
let mut editor = Editor::single_line(cx);
|
||||||
editor.set_placeholder_text("Filter channels, contacts", cx);
|
editor.set_placeholder_text("Filter...", cx);
|
||||||
editor
|
editor
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1156,25 +1156,21 @@ impl CollabPanel {
|
|||||||
let tooltip = format!("Follow {}", user.github_login);
|
let tooltip = format!("Follow {}", user.github_login);
|
||||||
|
|
||||||
ListItem::new(SharedString::from(user.github_login.clone()))
|
ListItem::new(SharedString::from(user.github_login.clone()))
|
||||||
.left_child(Avatar::new(user.avatar_uri.clone()))
|
.start_slot(Avatar::new(user.avatar_uri.clone()))
|
||||||
.child(
|
.child(Label::new(user.github_login.clone()))
|
||||||
h_stack()
|
.end_slot(if is_pending {
|
||||||
.w_full()
|
Label::new("Calling").color(Color::Muted).into_any_element()
|
||||||
.justify_between()
|
} else if is_current_user {
|
||||||
.child(Label::new(user.github_login.clone()))
|
IconButton::new("leave-call", Icon::Exit)
|
||||||
.child(if is_pending {
|
.style(ButtonStyle::Subtle)
|
||||||
Label::new("Calling").color(Color::Muted).into_any_element()
|
.on_click(cx.listener(move |this, _, cx| {
|
||||||
} else if is_current_user {
|
Self::leave_call(cx);
|
||||||
IconButton::new("leave-call", Icon::ArrowRight)
|
}))
|
||||||
.on_click(cx.listener(move |this, _, cx| {
|
.tooltip(|cx| Tooltip::text("Leave Call", cx))
|
||||||
Self::leave_call(cx);
|
.into_any_element()
|
||||||
}))
|
} else {
|
||||||
.tooltip(|cx| Tooltip::text("Leave Call", cx))
|
div().into_any_element()
|
||||||
.into_any_element()
|
})
|
||||||
} else {
|
|
||||||
div().into_any_element()
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.when_some(peer_id, |this, peer_id| {
|
.when_some(peer_id, |this, peer_id| {
|
||||||
this.tooltip(move |cx| Tooltip::text(tooltip.clone(), cx))
|
this.tooltip(move |cx| Tooltip::text(tooltip.clone(), cx))
|
||||||
.on_click(cx.listener(move |this, _, cx| {
|
.on_click(cx.listener(move |this, _, cx| {
|
||||||
@ -1212,8 +1208,12 @@ impl CollabPanel {
|
|||||||
.detach_and_log_err(cx);
|
.detach_and_log_err(cx);
|
||||||
});
|
});
|
||||||
}))
|
}))
|
||||||
.left_child(render_tree_branch(is_last, cx))
|
.start_slot(
|
||||||
.child(IconButton::new(0, Icon::Folder))
|
h_stack()
|
||||||
|
.gap_1()
|
||||||
|
.child(render_tree_branch(is_last, cx))
|
||||||
|
.child(IconButton::new(0, Icon::Folder)),
|
||||||
|
)
|
||||||
.child(Label::new(project_name.clone()))
|
.child(Label::new(project_name.clone()))
|
||||||
.tooltip(move |cx| Tooltip::text(format!("Open {}", project_name), cx))
|
.tooltip(move |cx| Tooltip::text(format!("Open {}", project_name), cx))
|
||||||
|
|
||||||
@ -1305,8 +1305,12 @@ impl CollabPanel {
|
|||||||
let id = peer_id.map_or(usize::MAX, |id| id.as_u64() as usize);
|
let id = peer_id.map_or(usize::MAX, |id| id.as_u64() as usize);
|
||||||
|
|
||||||
ListItem::new(("screen", id))
|
ListItem::new(("screen", id))
|
||||||
.left_child(render_tree_branch(is_last, cx))
|
.start_slot(
|
||||||
.child(IconButton::new(0, Icon::Screen))
|
h_stack()
|
||||||
|
.gap_1()
|
||||||
|
.child(render_tree_branch(is_last, cx))
|
||||||
|
.child(IconButton::new(0, Icon::Screen)),
|
||||||
|
)
|
||||||
.child(Label::new("Screen"))
|
.child(Label::new("Screen"))
|
||||||
.when_some(peer_id, |this, _| {
|
.when_some(peer_id, |this, _| {
|
||||||
this.on_click(cx.listener(move |this, _, cx| {
|
this.on_click(cx.listener(move |this, _, cx| {
|
||||||
@ -1372,9 +1376,13 @@ impl CollabPanel {
|
|||||||
.on_click(cx.listener(move |this, _, cx| {
|
.on_click(cx.listener(move |this, _, cx| {
|
||||||
this.open_channel_notes(channel_id, cx);
|
this.open_channel_notes(channel_id, cx);
|
||||||
}))
|
}))
|
||||||
.left_child(render_tree_branch(false, cx))
|
.start_slot(
|
||||||
.child(IconButton::new(0, Icon::File))
|
h_stack()
|
||||||
.child(Label::new("notes"))
|
.gap_1()
|
||||||
|
.child(render_tree_branch(false, cx))
|
||||||
|
.child(IconButton::new(0, Icon::File)),
|
||||||
|
)
|
||||||
|
.child(div().h_7().w_full().child(Label::new("notes")))
|
||||||
.tooltip(move |cx| Tooltip::text("Open Channel Notes", cx))
|
.tooltip(move |cx| Tooltip::text("Open Channel Notes", cx))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1387,8 +1395,12 @@ impl CollabPanel {
|
|||||||
.on_click(cx.listener(move |this, _, cx| {
|
.on_click(cx.listener(move |this, _, cx| {
|
||||||
this.join_channel_chat(channel_id, cx);
|
this.join_channel_chat(channel_id, cx);
|
||||||
}))
|
}))
|
||||||
.left_child(render_tree_branch(true, cx))
|
.start_slot(
|
||||||
.child(IconButton::new(0, Icon::MessageBubbles))
|
h_stack()
|
||||||
|
.gap_1()
|
||||||
|
.child(render_tree_branch(false, cx))
|
||||||
|
.child(IconButton::new(0, Icon::MessageBubbles)),
|
||||||
|
)
|
||||||
.child(Label::new("chat"))
|
.child(Label::new("chat"))
|
||||||
.tooltip(move |cx| Tooltip::text("Open Chat", cx))
|
.tooltip(move |cx| Tooltip::text("Open Chat", cx))
|
||||||
}
|
}
|
||||||
@ -2149,11 +2161,6 @@ impl CollabPanel {
|
|||||||
fn render_signed_in(&mut self, cx: &mut ViewContext<Self>) -> Div {
|
fn render_signed_in(&mut self, cx: &mut ViewContext<Self>) -> Div {
|
||||||
v_stack()
|
v_stack()
|
||||||
.size_full()
|
.size_full()
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.p_2()
|
|
||||||
.child(div().rounded(px(2.0)).child(self.filter_editor.clone())),
|
|
||||||
)
|
|
||||||
.child(
|
.child(
|
||||||
v_stack()
|
v_stack()
|
||||||
.size_full()
|
.size_full()
|
||||||
@ -2223,6 +2230,14 @@ impl CollabPanel {
|
|||||||
}
|
}
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
|
.child(
|
||||||
|
div().p_2().child(
|
||||||
|
div()
|
||||||
|
.border_primary(cx)
|
||||||
|
.border_t()
|
||||||
|
.child(self.filter_editor.clone()),
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_header(
|
fn render_header(
|
||||||
@ -2275,21 +2290,27 @@ impl CollabPanel {
|
|||||||
Section::ActiveCall => channel_link.map(|channel_link| {
|
Section::ActiveCall => channel_link.map(|channel_link| {
|
||||||
let channel_link_copy = channel_link.clone();
|
let channel_link_copy = channel_link.clone();
|
||||||
IconButton::new("channel-link", Icon::Copy)
|
IconButton::new("channel-link", Icon::Copy)
|
||||||
|
.icon_size(IconSize::Small)
|
||||||
|
.size(ButtonSize::None)
|
||||||
|
.visible_on_hover("section-header")
|
||||||
.on_click(move |_, cx| {
|
.on_click(move |_, cx| {
|
||||||
let item = ClipboardItem::new(channel_link_copy.clone());
|
let item = ClipboardItem::new(channel_link_copy.clone());
|
||||||
cx.write_to_clipboard(item)
|
cx.write_to_clipboard(item)
|
||||||
})
|
})
|
||||||
.tooltip(|cx| Tooltip::text("Copy channel link", cx))
|
.tooltip(|cx| Tooltip::text("Copy channel link", cx))
|
||||||
|
.into_any_element()
|
||||||
}),
|
}),
|
||||||
Section::Contacts => Some(
|
Section::Contacts => Some(
|
||||||
IconButton::new("add-contact", Icon::Plus)
|
IconButton::new("add-contact", Icon::Plus)
|
||||||
.on_click(cx.listener(|this, _, cx| this.toggle_contact_finder(cx)))
|
.on_click(cx.listener(|this, _, cx| this.toggle_contact_finder(cx)))
|
||||||
.tooltip(|cx| Tooltip::text("Search for new contact", cx)),
|
.tooltip(|cx| Tooltip::text("Search for new contact", cx))
|
||||||
|
.into_any_element(),
|
||||||
),
|
),
|
||||||
Section::Channels => Some(
|
Section::Channels => Some(
|
||||||
IconButton::new("add-channel", Icon::Plus)
|
IconButton::new("add-channel", Icon::Plus)
|
||||||
.on_click(cx.listener(|this, _, cx| this.new_root_channel(cx)))
|
.on_click(cx.listener(|this, _, cx| this.new_root_channel(cx)))
|
||||||
.tooltip(|cx| Tooltip::text("Create a channel", cx)),
|
.tooltip(|cx| Tooltip::text("Create a channel", cx))
|
||||||
|
.into_any_element(),
|
||||||
),
|
),
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
@ -2304,25 +2325,18 @@ impl CollabPanel {
|
|||||||
|
|
||||||
h_stack()
|
h_stack()
|
||||||
.w_full()
|
.w_full()
|
||||||
.map(|el| {
|
.group("section-header")
|
||||||
if can_collapse {
|
.child(
|
||||||
el.child(
|
ListHeader::new(text)
|
||||||
ListItem::new(text.clone())
|
.toggle(if can_collapse {
|
||||||
.child(div().w_full().child(Label::new(text)))
|
Some(!is_collapsed)
|
||||||
.selected(is_selected)
|
} else {
|
||||||
.toggle(Some(!is_collapsed))
|
None
|
||||||
.on_click(cx.listener(move |this, _, cx| {
|
})
|
||||||
this.toggle_section_expanded(section, cx)
|
.inset(true)
|
||||||
})),
|
.end_slot::<AnyElement>(button)
|
||||||
)
|
.selected(is_selected),
|
||||||
} else {
|
)
|
||||||
el.child(
|
|
||||||
ListHeader::new(text)
|
|
||||||
.when_some(button, |el, button| el.meta(button))
|
|
||||||
.selected(is_selected),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.when(section == Section::Channels, |el| {
|
.when(section == Section::Channels, |el| {
|
||||||
el.drag_over::<DraggedChannelView>(|style| {
|
el.drag_over::<DraggedChannelView>(|style| {
|
||||||
style.bg(cx.theme().colors().ghost_element_hover)
|
style.bg(cx.theme().colors().ghost_element_hover)
|
||||||
@ -2363,25 +2377,20 @@ impl CollabPanel {
|
|||||||
})
|
})
|
||||||
.when(!calling, |el| {
|
.when(!calling, |el| {
|
||||||
el.child(
|
el.child(
|
||||||
div()
|
IconButton::new("remove_contact", Icon::Close)
|
||||||
.id("remove_contact")
|
.icon_color(Color::Muted)
|
||||||
.invisible()
|
.visible_on_hover("")
|
||||||
.group_hover("", |style| style.visible())
|
.tooltip(|cx| Tooltip::text("Remove Contact", cx))
|
||||||
.child(
|
.on_click(cx.listener({
|
||||||
IconButton::new("remove_contact", Icon::Close)
|
let github_login = github_login.clone();
|
||||||
.icon_color(Color::Muted)
|
move |this, _, cx| {
|
||||||
.tooltip(|cx| Tooltip::text("Remove Contact", cx))
|
this.remove_contact(user_id, &github_login, cx);
|
||||||
.on_click(cx.listener({
|
}
|
||||||
let github_login = github_login.clone();
|
})),
|
||||||
move |this, _, cx| {
|
|
||||||
this.remove_contact(user_id, &github_login, cx);
|
|
||||||
}
|
|
||||||
})),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.left_child(
|
.start_slot(
|
||||||
// todo!() handle contacts with no avatar
|
// todo!() handle contacts with no avatar
|
||||||
Avatar::new(contact.user.avatar_uri.clone())
|
Avatar::new(contact.user.avatar_uri.clone())
|
||||||
.availability_indicator(if online { Some(!busy) } else { None }),
|
.availability_indicator(if online { Some(!busy) } else { None }),
|
||||||
@ -2460,7 +2469,7 @@ impl CollabPanel {
|
|||||||
.child(Label::new(github_login.clone()))
|
.child(Label::new(github_login.clone()))
|
||||||
.child(h_stack().children(controls)),
|
.child(h_stack().children(controls)),
|
||||||
)
|
)
|
||||||
.left_avatar(user.avatar_uri.clone())
|
.start_slot(Avatar::new(user.avatar_uri.clone()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_contact_placeholder(
|
fn render_contact_placeholder(
|
||||||
@ -2541,6 +2550,8 @@ impl CollabPanel {
|
|||||||
div()
|
div()
|
||||||
.id(channel_id as usize)
|
.id(channel_id as usize)
|
||||||
.group("")
|
.group("")
|
||||||
|
.flex()
|
||||||
|
.w_full()
|
||||||
.on_drag({
|
.on_drag({
|
||||||
let channel = channel.clone();
|
let channel = channel.clone();
|
||||||
move |cx| {
|
move |cx| {
|
||||||
@ -2566,67 +2577,10 @@ impl CollabPanel {
|
|||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
ListItem::new(channel_id as usize)
|
ListItem::new(channel_id as usize)
|
||||||
.indent_level(depth)
|
// Offset the indent depth by one to give us room to show the disclosure.
|
||||||
|
.indent_level(depth + 1)
|
||||||
.indent_step_size(cx.rem_size() * 14.0 / 16.0) // @todo()! @nate this is to step over the disclosure toggle
|
.indent_step_size(cx.rem_size() * 14.0 / 16.0) // @todo()! @nate this is to step over the disclosure toggle
|
||||||
.left_icon(if is_public { Icon::Public } else { Icon::Hash })
|
|
||||||
.selected(is_selected || is_active)
|
.selected(is_selected || is_active)
|
||||||
.child(
|
|
||||||
h_stack()
|
|
||||||
.w_full()
|
|
||||||
.justify_between()
|
|
||||||
.child(
|
|
||||||
h_stack()
|
|
||||||
.id(channel_id as usize)
|
|
||||||
.child(Label::new(channel.name.clone()))
|
|
||||||
.children(face_pile.map(|face_pile| face_pile.render(cx))),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
h_stack()
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.id("channel_chat")
|
|
||||||
.when(!has_messages_notification, |el| el.invisible())
|
|
||||||
.group_hover("", |style| style.visible())
|
|
||||||
.child(
|
|
||||||
IconButton::new(
|
|
||||||
"channel_chat",
|
|
||||||
Icon::MessageBubbles,
|
|
||||||
)
|
|
||||||
.icon_color(if has_messages_notification {
|
|
||||||
Color::Default
|
|
||||||
} else {
|
|
||||||
Color::Muted
|
|
||||||
})
|
|
||||||
.on_click(cx.listener(move |this, _, cx| {
|
|
||||||
this.join_channel_chat(channel_id, cx)
|
|
||||||
}))
|
|
||||||
.tooltip(|cx| {
|
|
||||||
Tooltip::text("Open channel chat", cx)
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.id("channel_notes")
|
|
||||||
.when(!has_notes_notification, |el| el.invisible())
|
|
||||||
.group_hover("", |style| style.visible())
|
|
||||||
.child(
|
|
||||||
IconButton::new("channel_notes", Icon::File)
|
|
||||||
.icon_color(if has_notes_notification {
|
|
||||||
Color::Default
|
|
||||||
} else {
|
|
||||||
Color::Muted
|
|
||||||
})
|
|
||||||
.on_click(cx.listener(move |this, _, cx| {
|
|
||||||
this.open_channel_notes(channel_id, cx)
|
|
||||||
}))
|
|
||||||
.tooltip(|cx| {
|
|
||||||
Tooltip::text("Open channel notes", cx)
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toggle(disclosed)
|
.toggle(disclosed)
|
||||||
.on_toggle(
|
.on_toggle(
|
||||||
cx.listener(move |this, _, cx| {
|
cx.listener(move |this, _, cx| {
|
||||||
@ -2646,7 +2600,49 @@ impl CollabPanel {
|
|||||||
move |this, event: &MouseDownEvent, cx| {
|
move |this, event: &MouseDownEvent, cx| {
|
||||||
this.deploy_channel_context_menu(event.position, channel_id, ix, cx)
|
this.deploy_channel_context_menu(event.position, channel_id, ix, cx)
|
||||||
},
|
},
|
||||||
)),
|
))
|
||||||
|
.start_slot(
|
||||||
|
IconElement::new(if is_public { Icon::Public } else { Icon::Hash })
|
||||||
|
.size(IconSize::Small)
|
||||||
|
.color(Color::Muted),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
h_stack()
|
||||||
|
.id(channel_id as usize)
|
||||||
|
.child(Label::new(channel.name.clone()))
|
||||||
|
.children(face_pile.map(|face_pile| face_pile.render(cx))),
|
||||||
|
)
|
||||||
|
.end_slot(
|
||||||
|
h_stack()
|
||||||
|
.child(
|
||||||
|
IconButton::new("channel_chat", Icon::MessageBubbles)
|
||||||
|
.icon_color(if has_messages_notification {
|
||||||
|
Color::Default
|
||||||
|
} else {
|
||||||
|
Color::Muted
|
||||||
|
})
|
||||||
|
.when(!has_messages_notification, |this| {
|
||||||
|
this.visible_on_hover("")
|
||||||
|
})
|
||||||
|
.on_click(cx.listener(move |this, _, cx| {
|
||||||
|
this.join_channel_chat(channel_id, cx)
|
||||||
|
}))
|
||||||
|
.tooltip(|cx| Tooltip::text("Open channel chat", cx)),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
IconButton::new("channel_notes", Icon::File)
|
||||||
|
.icon_color(if has_notes_notification {
|
||||||
|
Color::Default
|
||||||
|
} else {
|
||||||
|
Color::Muted
|
||||||
|
})
|
||||||
|
.when(!has_notes_notification, |this| this.visible_on_hover(""))
|
||||||
|
.on_click(cx.listener(move |this, _, cx| {
|
||||||
|
this.open_channel_notes(channel_id, cx)
|
||||||
|
}))
|
||||||
|
.tooltip(|cx| Tooltip::text("Open channel notes", cx)),
|
||||||
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.tooltip(|cx| Tooltip::text("Join channel", cx))
|
.tooltip(|cx| Tooltip::text("Join channel", cx))
|
||||||
|
|
||||||
@ -2962,7 +2958,11 @@ impl CollabPanel {
|
|||||||
let item = ListItem::new("channel-editor")
|
let item = ListItem::new("channel-editor")
|
||||||
.inset(false)
|
.inset(false)
|
||||||
.indent_level(depth)
|
.indent_level(depth)
|
||||||
.left_icon(Icon::Hash);
|
.start_slot(
|
||||||
|
IconElement::new(Icon::Hash)
|
||||||
|
.size(IconSize::Small)
|
||||||
|
.color(Color::Muted),
|
||||||
|
);
|
||||||
|
|
||||||
if let Some(pending_name) = self
|
if let Some(pending_name) = self
|
||||||
.channel_editing_state
|
.channel_editing_state
|
||||||
|
@ -9763,19 +9763,15 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> Rend
|
|||||||
.px_1p5()
|
.px_1p5()
|
||||||
.child(HighlightedLabel::new(line.clone(), highlights.clone()))
|
.child(HighlightedLabel::new(line.clone(), highlights.clone()))
|
||||||
.child(
|
.child(
|
||||||
div()
|
div().border().border_color(gpui::red()).child(
|
||||||
.border()
|
IconButton::new(copy_id.clone(), Icon::Copy)
|
||||||
.border_color(gpui::red())
|
.icon_color(Color::Muted)
|
||||||
.invisible()
|
.size(ButtonSize::Compact)
|
||||||
.group_hover(group_id, |style| style.visible())
|
.style(ButtonStyle::Transparent)
|
||||||
.child(
|
.visible_on_hover(group_id)
|
||||||
IconButton::new(copy_id.clone(), Icon::Copy)
|
.on_click(cx.listener(move |_, _, cx| write_to_clipboard))
|
||||||
.icon_color(Color::Muted)
|
.tooltip(|cx| Tooltip::text("Copy diagnostic message", cx)),
|
||||||
.size(ButtonSize::Compact)
|
),
|
||||||
.style(ButtonStyle::Transparent)
|
|
||||||
.on_click(cx.listener(move |_, _, cx| write_to_clipboard))
|
|
||||||
.tooltip(|cx| Tooltip::text("Copy diagnostic message", cx)),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}))
|
}))
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
|
@ -53,7 +53,7 @@ pub struct FeedbackModal {
|
|||||||
email_address_editor: View<Editor>,
|
email_address_editor: View<Editor>,
|
||||||
awaiting_submission: bool,
|
awaiting_submission: bool,
|
||||||
user_submitted: bool,
|
user_submitted: bool,
|
||||||
discarded: bool,
|
user_discarded: bool,
|
||||||
character_count: i32,
|
character_count: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -71,7 +71,7 @@ impl ModalView for FeedbackModal {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.discarded {
|
if self.user_discarded {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -85,7 +85,7 @@ impl ModalView for FeedbackModal {
|
|||||||
cx.spawn(move |this, mut cx| async move {
|
cx.spawn(move |this, mut cx| async move {
|
||||||
if answer.await.ok() == Some(0) {
|
if answer.await.ok() == Some(0) {
|
||||||
this.update(&mut cx, |this, cx| {
|
this.update(&mut cx, |this, cx| {
|
||||||
this.discarded = true;
|
this.user_discarded = true;
|
||||||
cx.emit(DismissEvent)
|
cx.emit(DismissEvent)
|
||||||
})
|
})
|
||||||
.log_err();
|
.log_err();
|
||||||
@ -184,7 +184,7 @@ impl FeedbackModal {
|
|||||||
email_address_editor,
|
email_address_editor,
|
||||||
awaiting_submission: false,
|
awaiting_submission: false,
|
||||||
user_submitted: false,
|
user_submitted: false,
|
||||||
discarded: false,
|
user_discarded: false,
|
||||||
character_count: 0,
|
character_count: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -309,7 +309,6 @@ impl FeedbackModal {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Escape button calls dismiss
|
|
||||||
fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
|
fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
|
||||||
cx.emit(DismissEvent)
|
cx.emit(DismissEvent)
|
||||||
}
|
}
|
||||||
|
@ -893,7 +893,7 @@ impl Interactivity {
|
|||||||
if style
|
if style
|
||||||
.background
|
.background
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.is_some_and(|fill| fill.color().is_some_and(|color| !color.is_transparent()))
|
.is_some_and(|fill| fill.color().is_some())
|
||||||
{
|
{
|
||||||
cx.with_z_index(style.z_index.unwrap_or(0), |cx| cx.add_opaque_layer(bounds))
|
cx.with_z_index(style.z_index.unwrap_or(0), |cx| cx.add_opaque_layer(bounds))
|
||||||
}
|
}
|
||||||
@ -1273,78 +1273,84 @@ impl Interactivity {
|
|||||||
let mut style = Style::default();
|
let mut style = Style::default();
|
||||||
style.refine(&self.base_style);
|
style.refine(&self.base_style);
|
||||||
|
|
||||||
if let Some(focus_handle) = self.tracked_focus_handle.as_ref() {
|
cx.with_z_index(style.z_index.unwrap_or(0), |cx| {
|
||||||
if let Some(in_focus_style) = self.in_focus_style.as_ref() {
|
if let Some(focus_handle) = self.tracked_focus_handle.as_ref() {
|
||||||
if focus_handle.within_focused(cx) {
|
if let Some(in_focus_style) = self.in_focus_style.as_ref() {
|
||||||
style.refine(in_focus_style);
|
if focus_handle.within_focused(cx) {
|
||||||
|
style.refine(in_focus_style);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(focus_style) = self.focus_style.as_ref() {
|
if let Some(focus_style) = self.focus_style.as_ref() {
|
||||||
if focus_handle.is_focused(cx) {
|
if focus_handle.is_focused(cx) {
|
||||||
style.refine(focus_style);
|
style.refine(focus_style);
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(bounds) = bounds {
|
|
||||||
let mouse_position = cx.mouse_position();
|
|
||||||
if let Some(group_hover) = self.group_hover_style.as_ref() {
|
|
||||||
if let Some(group_bounds) = GroupBounds::get(&group_hover.group, cx) {
|
|
||||||
if group_bounds.contains(&mouse_position)
|
|
||||||
&& cx.was_top_layer(&mouse_position, cx.stacking_order())
|
|
||||||
{
|
|
||||||
style.refine(&group_hover.style);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(hover_style) = self.hover_style.as_ref() {
|
|
||||||
if bounds
|
|
||||||
.intersect(&cx.content_mask().bounds)
|
|
||||||
.contains(&mouse_position)
|
|
||||||
&& cx.was_top_layer(&mouse_position, cx.stacking_order())
|
|
||||||
{
|
|
||||||
style.refine(hover_style);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(drag) = cx.active_drag.take() {
|
if let Some(bounds) = bounds {
|
||||||
for (state_type, group_drag_style) in &self.group_drag_over_styles {
|
let mouse_position = cx.mouse_position();
|
||||||
if let Some(group_bounds) = GroupBounds::get(&group_drag_style.group, cx) {
|
if let Some(group_hover) = self.group_hover_style.as_ref() {
|
||||||
if *state_type == drag.view.entity_type()
|
if let Some(group_bounds) = GroupBounds::get(&group_hover.group, cx) {
|
||||||
&& group_bounds.contains(&mouse_position)
|
if group_bounds.contains(&mouse_position)
|
||||||
|
&& cx.was_top_layer(&mouse_position, cx.stacking_order())
|
||||||
{
|
{
|
||||||
style.refine(&group_drag_style.style);
|
style.refine(&group_hover.style);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if let Some(hover_style) = self.hover_style.as_ref() {
|
||||||
for (state_type, drag_over_style) in &self.drag_over_styles {
|
if bounds
|
||||||
if *state_type == drag.view.entity_type()
|
.intersect(&cx.content_mask().bounds)
|
||||||
&& bounds
|
.contains(&mouse_position)
|
||||||
.intersect(&cx.content_mask().bounds)
|
&& cx.was_top_layer(&mouse_position, cx.stacking_order())
|
||||||
.contains(&mouse_position)
|
|
||||||
{
|
{
|
||||||
style.refine(drag_over_style);
|
style.refine(hover_style);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cx.active_drag = Some(drag);
|
if let Some(drag) = cx.active_drag.take() {
|
||||||
}
|
for (state_type, group_drag_style) in &self.group_drag_over_styles {
|
||||||
}
|
if let Some(group_bounds) = GroupBounds::get(&group_drag_style.group, cx) {
|
||||||
|
if *state_type == drag.view.entity_type()
|
||||||
|
&& group_bounds.contains(&mouse_position)
|
||||||
|
{
|
||||||
|
style.refine(&group_drag_style.style);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let clicked_state = element_state.clicked_state.borrow();
|
for (state_type, drag_over_style) in &self.drag_over_styles {
|
||||||
if clicked_state.group {
|
if *state_type == drag.view.entity_type()
|
||||||
if let Some(group) = self.group_active_style.as_ref() {
|
&& bounds
|
||||||
style.refine(&group.style)
|
.intersect(&cx.content_mask().bounds)
|
||||||
}
|
.contains(&mouse_position)
|
||||||
}
|
&& cx.was_top_layer_under_active_drag(
|
||||||
|
&mouse_position,
|
||||||
|
cx.stacking_order(),
|
||||||
|
)
|
||||||
|
{
|
||||||
|
style.refine(drag_over_style);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(active_style) = self.active_style.as_ref() {
|
cx.active_drag = Some(drag);
|
||||||
if clicked_state.element {
|
}
|
||||||
style.refine(active_style)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
let clicked_state = element_state.clicked_state.borrow();
|
||||||
|
if clicked_state.group {
|
||||||
|
if let Some(group) = self.group_active_style.as_ref() {
|
||||||
|
style.refine(&group.style)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(active_style) = self.active_style.as_ref() {
|
||||||
|
if clicked_state.element {
|
||||||
|
style.refine(active_style)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
style
|
style
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,7 @@ pub type LayerId = u32;
|
|||||||
pub type DrawOrder = u32;
|
pub type DrawOrder = u32;
|
||||||
|
|
||||||
pub(crate) struct SceneBuilder {
|
pub(crate) struct SceneBuilder {
|
||||||
|
last_order: Option<(StackingOrder, LayerId)>,
|
||||||
layers_by_order: BTreeMap<StackingOrder, LayerId>,
|
layers_by_order: BTreeMap<StackingOrder, LayerId>,
|
||||||
splitter: BspSplitter<(PrimitiveKind, usize)>,
|
splitter: BspSplitter<(PrimitiveKind, usize)>,
|
||||||
shadows: Vec<Shadow>,
|
shadows: Vec<Shadow>,
|
||||||
@ -31,6 +32,7 @@ pub(crate) struct SceneBuilder {
|
|||||||
impl Default for SceneBuilder {
|
impl Default for SceneBuilder {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
SceneBuilder {
|
SceneBuilder {
|
||||||
|
last_order: None,
|
||||||
layers_by_order: BTreeMap::new(),
|
layers_by_order: BTreeMap::new(),
|
||||||
splitter: BspSplitter::new(),
|
splitter: BspSplitter::new(),
|
||||||
shadows: Vec::new(),
|
shadows: Vec::new(),
|
||||||
@ -52,6 +54,7 @@ impl SceneBuilder {
|
|||||||
layer_z_values[*layer_id as usize] = ix as f32 / self.layers_by_order.len() as f32;
|
layer_z_values[*layer_id as usize] = ix as f32 / self.layers_by_order.len() as f32;
|
||||||
}
|
}
|
||||||
self.layers_by_order.clear();
|
self.layers_by_order.clear();
|
||||||
|
self.last_order = None;
|
||||||
|
|
||||||
// Add all primitives to the BSP splitter to determine draw order
|
// Add all primitives to the BSP splitter to determine draw order
|
||||||
self.splitter.reset();
|
self.splitter.reset();
|
||||||
@ -156,14 +159,7 @@ impl SceneBuilder {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let layer_id = if let Some(layer_id) = self.layers_by_order.get(order) {
|
let layer_id = self.layer_id_for_order(order);
|
||||||
*layer_id
|
|
||||||
} else {
|
|
||||||
let next_id = self.layers_by_order.len() as LayerId;
|
|
||||||
self.layers_by_order.insert(order.clone(), next_id);
|
|
||||||
next_id
|
|
||||||
};
|
|
||||||
|
|
||||||
match primitive {
|
match primitive {
|
||||||
Primitive::Shadow(mut shadow) => {
|
Primitive::Shadow(mut shadow) => {
|
||||||
shadow.order = layer_id;
|
shadow.order = layer_id;
|
||||||
@ -196,6 +192,24 @@ impl SceneBuilder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn layer_id_for_order(&mut self, order: &StackingOrder) -> u32 {
|
||||||
|
if let Some((last_order, last_layer_id)) = self.last_order.as_ref() {
|
||||||
|
if last_order == order {
|
||||||
|
return *last_layer_id;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let layer_id = if let Some(layer_id) = self.layers_by_order.get(order) {
|
||||||
|
*layer_id
|
||||||
|
} else {
|
||||||
|
let next_id = self.layers_by_order.len() as LayerId;
|
||||||
|
self.layers_by_order.insert(order.clone(), next_id);
|
||||||
|
next_id
|
||||||
|
};
|
||||||
|
self.last_order = Some((order.clone(), layer_id));
|
||||||
|
layer_id
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Scene {
|
pub struct Scene {
|
||||||
|
@ -16,6 +16,7 @@ menu = { package = "menu2", path = "../menu2" }
|
|||||||
settings = { package = "settings2", path = "../settings2" }
|
settings = { package = "settings2", path = "../settings2" }
|
||||||
util = { path = "../util" }
|
util = { path = "../util" }
|
||||||
theme = { package = "theme2", path = "../theme2" }
|
theme = { package = "theme2", path = "../theme2" }
|
||||||
|
workspace = { package = "workspace2", path = "../workspace2"}
|
||||||
|
|
||||||
parking_lot.workspace = true
|
parking_lot.workspace = true
|
||||||
|
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
use editor::Editor;
|
use editor::Editor;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, prelude::*, rems, uniform_list, AnyElement, AppContext, DismissEvent, Div, EventEmitter,
|
div, prelude::*, rems, uniform_list, AnyElement, AppContext, DismissEvent, Div, EventEmitter,
|
||||||
FocusHandle, FocusableView, MouseButton, MouseDownEvent, Render, Task, UniformListScrollHandle,
|
FocusHandle, FocusableView, Length, MouseButton, MouseDownEvent, Render, Task,
|
||||||
View, ViewContext, WindowContext,
|
UniformListScrollHandle, View, ViewContext, WindowContext,
|
||||||
};
|
};
|
||||||
use std::{cmp, sync::Arc};
|
use std::{cmp, sync::Arc};
|
||||||
use ui::{prelude::*, v_stack, Color, Divider, Label};
|
use ui::{prelude::*, v_stack, Color, Divider, Label};
|
||||||
|
use workspace::ModalView;
|
||||||
|
|
||||||
pub struct Picker<D: PickerDelegate> {
|
pub struct Picker<D: PickerDelegate> {
|
||||||
pub delegate: D,
|
pub delegate: D,
|
||||||
@ -13,6 +14,7 @@ pub struct Picker<D: PickerDelegate> {
|
|||||||
editor: View<Editor>,
|
editor: View<Editor>,
|
||||||
pending_update_matches: Option<Task<()>>,
|
pending_update_matches: Option<Task<()>>,
|
||||||
confirm_on_update: Option<bool>,
|
confirm_on_update: Option<bool>,
|
||||||
|
width: Option<Length>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait PickerDelegate: Sized + 'static {
|
pub trait PickerDelegate: Sized + 'static {
|
||||||
@ -55,11 +57,17 @@ impl<D: PickerDelegate> Picker<D> {
|
|||||||
scroll_handle: UniformListScrollHandle::new(),
|
scroll_handle: UniformListScrollHandle::new(),
|
||||||
pending_update_matches: None,
|
pending_update_matches: None,
|
||||||
confirm_on_update: None,
|
confirm_on_update: None,
|
||||||
|
width: None,
|
||||||
};
|
};
|
||||||
this.update_matches("".to_string(), cx);
|
this.update_matches("".to_string(), cx);
|
||||||
this
|
this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn width(mut self, width: impl Into<gpui::Length>) -> Self {
|
||||||
|
self.width = Some(width.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn focus(&self, cx: &mut WindowContext) {
|
pub fn focus(&self, cx: &mut WindowContext) {
|
||||||
self.editor.update(cx, |editor, cx| editor.focus(cx));
|
self.editor.update(cx, |editor, cx| editor.focus(cx));
|
||||||
}
|
}
|
||||||
@ -197,6 +205,7 @@ impl<D: PickerDelegate> Picker<D> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<D: PickerDelegate> EventEmitter<DismissEvent> for Picker<D> {}
|
impl<D: PickerDelegate> EventEmitter<DismissEvent> for Picker<D> {}
|
||||||
|
impl<D: PickerDelegate> ModalView for Picker<D> {}
|
||||||
|
|
||||||
impl<D: PickerDelegate> Render for Picker<D> {
|
impl<D: PickerDelegate> Render for Picker<D> {
|
||||||
type Element = Div;
|
type Element = Div;
|
||||||
@ -221,6 +230,9 @@ impl<D: PickerDelegate> Render for Picker<D> {
|
|||||||
div()
|
div()
|
||||||
.key_context("picker")
|
.key_context("picker")
|
||||||
.size_full()
|
.size_full()
|
||||||
|
.when_some(self.width, |el, width| {
|
||||||
|
el.w(width)
|
||||||
|
})
|
||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
.elevation_3(cx)
|
.elevation_3(cx)
|
||||||
.on_action(cx.listener(Self::select_next))
|
.on_action(cx.listener(Self::select_next))
|
||||||
@ -271,7 +283,6 @@ impl<D: PickerDelegate> Render for Picker<D> {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
.track_scroll(self.scroll_handle.clone())
|
.track_scroll(self.scroll_handle.clone())
|
||||||
.p_1()
|
|
||||||
)
|
)
|
||||||
.max_h_72()
|
.max_h_72()
|
||||||
.overflow_hidden(),
|
.overflow_hidden(),
|
||||||
|
37
crates/project_symbols2/Cargo.toml
Normal file
37
crates/project_symbols2/Cargo.toml
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
[package]
|
||||||
|
name = "project_symbols2"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "src/project_symbols.rs"
|
||||||
|
doctest = false
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
editor = { package = "editor2", path = "../editor2" }
|
||||||
|
fuzzy = {package = "fuzzy2", path = "../fuzzy2" }
|
||||||
|
gpui = {package = "gpui2", path = "../gpui2" }
|
||||||
|
picker = {package = "picker2", path = "../picker2" }
|
||||||
|
project = { package = "project2", path = "../project2" }
|
||||||
|
text = {package = "text2", path = "../text2" }
|
||||||
|
settings = {package = "settings2", path = "../settings2" }
|
||||||
|
workspace = {package = "workspace2", path = "../workspace2" }
|
||||||
|
theme = { package = "theme2", path = "../theme2" }
|
||||||
|
util = { path = "../util" }
|
||||||
|
|
||||||
|
anyhow.workspace = true
|
||||||
|
ordered-float.workspace = true
|
||||||
|
postage.workspace = true
|
||||||
|
smol.workspace = true
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
futures.workspace = true
|
||||||
|
editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
|
||||||
|
settings = { package = "settings2", path = "../settings2", features = ["test-support"] }
|
||||||
|
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
|
||||||
|
language = { package = "language2", path = "../language2", features = ["test-support"] }
|
||||||
|
lsp = { package = "lsp2", path = "../lsp2", features = ["test-support"] }
|
||||||
|
project = { package = "project2", path = "../project2", features = ["test-support"] }
|
||||||
|
theme = { package = "theme2", path = "../theme2", features = ["test-support"] }
|
||||||
|
workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] }
|
411
crates/project_symbols2/src/project_symbols.rs
Normal file
411
crates/project_symbols2/src/project_symbols.rs
Normal file
@ -0,0 +1,411 @@
|
|||||||
|
use editor::{scroll::autoscroll::Autoscroll, styled_runs_for_code_label, Bias, Editor};
|
||||||
|
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||||
|
use gpui::{
|
||||||
|
actions, rems, AppContext, DismissEvent, FontWeight, Model, ParentElement, StyledText, Task,
|
||||||
|
View, ViewContext, WeakView,
|
||||||
|
};
|
||||||
|
use ordered_float::OrderedFloat;
|
||||||
|
use picker::{Picker, PickerDelegate};
|
||||||
|
use project::{Project, Symbol};
|
||||||
|
use std::{borrow::Cow, cmp::Reverse, sync::Arc};
|
||||||
|
use theme::ActiveTheme;
|
||||||
|
use util::ResultExt;
|
||||||
|
use workspace::{
|
||||||
|
ui::{v_stack, Color, Label, LabelCommon, LabelLike, ListItem, Selectable},
|
||||||
|
Workspace,
|
||||||
|
};
|
||||||
|
|
||||||
|
actions!(project_symbols, [Toggle]);
|
||||||
|
|
||||||
|
pub fn init(cx: &mut AppContext) {
|
||||||
|
cx.observe_new_views(
|
||||||
|
|workspace: &mut Workspace, _: &mut ViewContext<Workspace>| {
|
||||||
|
workspace.register_action(|workspace, _: &Toggle, cx| {
|
||||||
|
let project = workspace.project().clone();
|
||||||
|
let handle = cx.view().downgrade();
|
||||||
|
workspace.toggle_modal(cx, move |cx| {
|
||||||
|
let delegate = ProjectSymbolsDelegate::new(handle, project);
|
||||||
|
Picker::new(delegate, cx).width(rems(34.))
|
||||||
|
})
|
||||||
|
});
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type ProjectSymbols = View<Picker<ProjectSymbolsDelegate>>;
|
||||||
|
|
||||||
|
pub struct ProjectSymbolsDelegate {
|
||||||
|
workspace: WeakView<Workspace>,
|
||||||
|
project: Model<Project>,
|
||||||
|
selected_match_index: usize,
|
||||||
|
symbols: Vec<Symbol>,
|
||||||
|
visible_match_candidates: Vec<StringMatchCandidate>,
|
||||||
|
external_match_candidates: Vec<StringMatchCandidate>,
|
||||||
|
show_worktree_root_name: bool,
|
||||||
|
matches: Vec<StringMatch>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProjectSymbolsDelegate {
|
||||||
|
fn new(workspace: WeakView<Workspace>, project: Model<Project>) -> Self {
|
||||||
|
Self {
|
||||||
|
workspace,
|
||||||
|
project,
|
||||||
|
selected_match_index: 0,
|
||||||
|
symbols: Default::default(),
|
||||||
|
visible_match_candidates: Default::default(),
|
||||||
|
external_match_candidates: Default::default(),
|
||||||
|
matches: Default::default(),
|
||||||
|
show_worktree_root_name: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn filter(&mut self, query: &str, cx: &mut ViewContext<Picker<Self>>) {
|
||||||
|
const MAX_MATCHES: usize = 100;
|
||||||
|
let mut visible_matches = cx.background_executor().block(fuzzy::match_strings(
|
||||||
|
&self.visible_match_candidates,
|
||||||
|
query,
|
||||||
|
false,
|
||||||
|
MAX_MATCHES,
|
||||||
|
&Default::default(),
|
||||||
|
cx.background_executor().clone(),
|
||||||
|
));
|
||||||
|
let mut external_matches = cx.background_executor().block(fuzzy::match_strings(
|
||||||
|
&self.external_match_candidates,
|
||||||
|
query,
|
||||||
|
false,
|
||||||
|
MAX_MATCHES - visible_matches.len().min(MAX_MATCHES),
|
||||||
|
&Default::default(),
|
||||||
|
cx.background_executor().clone(),
|
||||||
|
));
|
||||||
|
let sort_key_for_match = |mat: &StringMatch| {
|
||||||
|
let symbol = &self.symbols[mat.candidate_id];
|
||||||
|
(
|
||||||
|
Reverse(OrderedFloat(mat.score)),
|
||||||
|
&symbol.label.text[symbol.label.filter_range.clone()],
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
visible_matches.sort_unstable_by_key(sort_key_for_match);
|
||||||
|
external_matches.sort_unstable_by_key(sort_key_for_match);
|
||||||
|
let mut matches = visible_matches;
|
||||||
|
matches.append(&mut external_matches);
|
||||||
|
|
||||||
|
for mat in &mut matches {
|
||||||
|
let symbol = &self.symbols[mat.candidate_id];
|
||||||
|
let filter_start = symbol.label.filter_range.start;
|
||||||
|
for position in &mut mat.positions {
|
||||||
|
*position += filter_start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.matches = matches;
|
||||||
|
self.set_selected_index(0, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PickerDelegate for ProjectSymbolsDelegate {
|
||||||
|
type ListItem = ListItem;
|
||||||
|
fn placeholder_text(&self) -> Arc<str> {
|
||||||
|
"Search project symbols...".into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn confirm(&mut self, secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
|
||||||
|
if let Some(symbol) = self
|
||||||
|
.matches
|
||||||
|
.get(self.selected_match_index)
|
||||||
|
.map(|mat| self.symbols[mat.candidate_id].clone())
|
||||||
|
{
|
||||||
|
let buffer = self.project.update(cx, |project, cx| {
|
||||||
|
project.open_buffer_for_symbol(&symbol, cx)
|
||||||
|
});
|
||||||
|
let symbol = symbol.clone();
|
||||||
|
let workspace = self.workspace.clone();
|
||||||
|
cx.spawn(|_, mut cx| async move {
|
||||||
|
let buffer = buffer.await?;
|
||||||
|
workspace.update(&mut cx, |workspace, cx| {
|
||||||
|
let position = buffer
|
||||||
|
.read(cx)
|
||||||
|
.clip_point_utf16(symbol.range.start, Bias::Left);
|
||||||
|
|
||||||
|
let editor = if secondary {
|
||||||
|
workspace.split_project_item::<Editor>(buffer, cx)
|
||||||
|
} else {
|
||||||
|
workspace.open_project_item::<Editor>(buffer, cx)
|
||||||
|
};
|
||||||
|
|
||||||
|
editor.update(cx, |editor, cx| {
|
||||||
|
editor.change_selections(Some(Autoscroll::center()), cx, |s| {
|
||||||
|
s.select_ranges([position..position])
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})?;
|
||||||
|
Ok::<_, anyhow::Error>(())
|
||||||
|
})
|
||||||
|
.detach_and_log_err(cx);
|
||||||
|
cx.emit(DismissEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dismissed(&mut self, _cx: &mut ViewContext<Picker<Self>>) {}
|
||||||
|
|
||||||
|
fn match_count(&self) -> usize {
|
||||||
|
self.matches.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn selected_index(&self) -> usize {
|
||||||
|
self.selected_match_index
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext<Picker<Self>>) {
|
||||||
|
self.selected_match_index = ix;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
|
||||||
|
self.filter(&query, cx);
|
||||||
|
self.show_worktree_root_name = self.project.read(cx).visible_worktrees(cx).count() > 1;
|
||||||
|
let symbols = self
|
||||||
|
.project
|
||||||
|
.update(cx, |project, cx| project.symbols(&query, cx));
|
||||||
|
cx.spawn(|this, mut cx| async move {
|
||||||
|
let symbols = symbols.await.log_err();
|
||||||
|
if let Some(symbols) = symbols {
|
||||||
|
this.update(&mut cx, |this, cx| {
|
||||||
|
let delegate = &mut this.delegate;
|
||||||
|
let project = delegate.project.read(cx);
|
||||||
|
let (visible_match_candidates, external_match_candidates) = symbols
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(id, symbol)| {
|
||||||
|
StringMatchCandidate::new(
|
||||||
|
id,
|
||||||
|
symbol.label.text[symbol.label.filter_range.clone()].to_string(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.partition(|candidate| {
|
||||||
|
project
|
||||||
|
.entry_for_path(&symbols[candidate.id].path, cx)
|
||||||
|
.map_or(false, |e| !e.is_ignored)
|
||||||
|
});
|
||||||
|
|
||||||
|
delegate.visible_match_candidates = visible_match_candidates;
|
||||||
|
delegate.external_match_candidates = external_match_candidates;
|
||||||
|
delegate.symbols = symbols;
|
||||||
|
delegate.filter(&query, cx);
|
||||||
|
})
|
||||||
|
.log_err();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_match(
|
||||||
|
&self,
|
||||||
|
ix: usize,
|
||||||
|
selected: bool,
|
||||||
|
cx: &mut ViewContext<Picker<Self>>,
|
||||||
|
) -> Option<Self::ListItem> {
|
||||||
|
let string_match = &self.matches[ix];
|
||||||
|
let symbol = &self.symbols[string_match.candidate_id];
|
||||||
|
let syntax_runs = styled_runs_for_code_label(&symbol.label, cx.theme().syntax());
|
||||||
|
|
||||||
|
let mut path = symbol.path.path.to_string_lossy();
|
||||||
|
if self.show_worktree_root_name {
|
||||||
|
let project = self.project.read(cx);
|
||||||
|
if let Some(worktree) = project.worktree_for_id(symbol.path.worktree_id, cx) {
|
||||||
|
path = Cow::Owned(format!(
|
||||||
|
"{}{}{}",
|
||||||
|
worktree.read(cx).root_name(),
|
||||||
|
std::path::MAIN_SEPARATOR,
|
||||||
|
path.as_ref()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let label = symbol.label.text.clone();
|
||||||
|
let path = path.to_string().clone();
|
||||||
|
|
||||||
|
let highlights = gpui::combine_highlights(
|
||||||
|
string_match
|
||||||
|
.positions
|
||||||
|
.iter()
|
||||||
|
.map(|pos| (*pos..pos + 1, FontWeight::BOLD.into())),
|
||||||
|
syntax_runs.map(|(range, mut highlight)| {
|
||||||
|
// Ignore font weight for syntax highlighting, as we'll use it
|
||||||
|
// for fuzzy matches.
|
||||||
|
highlight.font_weight = None;
|
||||||
|
(range, highlight)
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
Some(
|
||||||
|
ListItem::new(ix).inset(true).selected(selected).child(
|
||||||
|
// todo!() combine_syntax_and_fuzzy_match_highlights()
|
||||||
|
v_stack()
|
||||||
|
.child(
|
||||||
|
LabelLike::new().child(
|
||||||
|
StyledText::new(label)
|
||||||
|
.with_highlights(&cx.text_style().clone(), highlights),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(Label::new(path).color(Color::Muted)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use futures::StreamExt;
|
||||||
|
use gpui::{serde_json::json, TestAppContext, VisualContext};
|
||||||
|
use language::{FakeLspAdapter, Language, LanguageConfig};
|
||||||
|
use project::FakeFs;
|
||||||
|
use settings::SettingsStore;
|
||||||
|
use std::{path::Path, sync::Arc};
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_project_symbols(cx: &mut TestAppContext) {
|
||||||
|
init_test(cx);
|
||||||
|
|
||||||
|
let mut language = Language::new(
|
||||||
|
LanguageConfig {
|
||||||
|
name: "Rust".into(),
|
||||||
|
path_suffixes: vec!["rs".to_string()],
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
let mut fake_servers = language
|
||||||
|
.set_fake_lsp_adapter(Arc::<FakeLspAdapter>::default())
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let fs = FakeFs::new(cx.executor());
|
||||||
|
fs.insert_tree("/dir", json!({ "test.rs": "" })).await;
|
||||||
|
|
||||||
|
let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
|
||||||
|
project.update(cx, |project, _| project.languages().add(Arc::new(language)));
|
||||||
|
|
||||||
|
let _buffer = project
|
||||||
|
.update(cx, |project, cx| {
|
||||||
|
project.open_local_buffer("/dir/test.rs", cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Set up fake language server to return fuzzy matches against
|
||||||
|
// a fixed set of symbol names.
|
||||||
|
let fake_symbols = [
|
||||||
|
symbol("one", "/external"),
|
||||||
|
symbol("ton", "/dir/test.rs"),
|
||||||
|
symbol("uno", "/dir/test.rs"),
|
||||||
|
];
|
||||||
|
let fake_server = fake_servers.next().await.unwrap();
|
||||||
|
fake_server.handle_request::<lsp::WorkspaceSymbolRequest, _, _>(
|
||||||
|
move |params: lsp::WorkspaceSymbolParams, cx| {
|
||||||
|
let executor = cx.background_executor().clone();
|
||||||
|
let fake_symbols = fake_symbols.clone();
|
||||||
|
async move {
|
||||||
|
let candidates = fake_symbols
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(id, symbol)| StringMatchCandidate::new(id, symbol.name.clone()))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let matches = if params.query.is_empty() {
|
||||||
|
Vec::new()
|
||||||
|
} else {
|
||||||
|
fuzzy::match_strings(
|
||||||
|
&candidates,
|
||||||
|
¶ms.query,
|
||||||
|
true,
|
||||||
|
100,
|
||||||
|
&Default::default(),
|
||||||
|
executor.clone(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Some(lsp::WorkspaceSymbolResponse::Flat(
|
||||||
|
matches
|
||||||
|
.into_iter()
|
||||||
|
.map(|mat| fake_symbols[mat.candidate_id].clone())
|
||||||
|
.collect(),
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
|
||||||
|
|
||||||
|
// Create the project symbols view.
|
||||||
|
let symbols = cx.build_view(|cx| {
|
||||||
|
Picker::new(
|
||||||
|
ProjectSymbolsDelegate::new(workspace.downgrade(), project.clone()),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Spawn multiples updates before the first update completes,
|
||||||
|
// such that in the end, there are no matches. Testing for regression:
|
||||||
|
// https://github.com/zed-industries/zed/issues/861
|
||||||
|
symbols.update(cx, |p, cx| {
|
||||||
|
p.update_matches("o".to_string(), cx);
|
||||||
|
p.update_matches("on".to_string(), cx);
|
||||||
|
p.update_matches("onex".to_string(), cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.run_until_parked();
|
||||||
|
symbols.update(cx, |symbols, _| {
|
||||||
|
assert_eq!(symbols.delegate.matches.len(), 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Spawn more updates such that in the end, there are matches.
|
||||||
|
symbols.update(cx, |p, cx| {
|
||||||
|
p.update_matches("one".to_string(), cx);
|
||||||
|
p.update_matches("on".to_string(), cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.run_until_parked();
|
||||||
|
symbols.update(cx, |symbols, _| {
|
||||||
|
let delegate = &symbols.delegate;
|
||||||
|
assert_eq!(delegate.matches.len(), 2);
|
||||||
|
assert_eq!(delegate.matches[0].string, "ton");
|
||||||
|
assert_eq!(delegate.matches[1].string, "one");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Spawn more updates such that in the end, there are again no matches.
|
||||||
|
symbols.update(cx, |p, cx| {
|
||||||
|
p.update_matches("o".to_string(), cx);
|
||||||
|
p.update_matches("".to_string(), cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.run_until_parked();
|
||||||
|
symbols.update(cx, |symbols, _| {
|
||||||
|
assert_eq!(symbols.delegate.matches.len(), 0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init_test(cx: &mut TestAppContext) {
|
||||||
|
cx.update(|cx| {
|
||||||
|
let store = SettingsStore::test(cx);
|
||||||
|
cx.set_global(store);
|
||||||
|
theme::init(theme::LoadThemes::JustBase, cx);
|
||||||
|
language::init(cx);
|
||||||
|
Project::init_settings(cx);
|
||||||
|
workspace::init_settings(cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn symbol(name: &str, path: impl AsRef<Path>) -> lsp::SymbolInformation {
|
||||||
|
#[allow(deprecated)]
|
||||||
|
lsp::SymbolInformation {
|
||||||
|
name: name.to_string(),
|
||||||
|
kind: lsp::SymbolKind::FUNCTION,
|
||||||
|
tags: None,
|
||||||
|
deprecated: None,
|
||||||
|
container_name: None,
|
||||||
|
location: lsp::Location::new(
|
||||||
|
lsp::Url::from_file_path(path.as_ref()).unwrap(),
|
||||||
|
lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -2,7 +2,6 @@ use gpui::{relative, DefiniteLength};
|
|||||||
use gpui::{rems, transparent_black, AnyElement, AnyView, ClickEvent, Div, Hsla, Rems, Stateful};
|
use gpui::{rems, transparent_black, AnyElement, AnyView, ClickEvent, Div, Hsla, Rems, Stateful};
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
|
|
||||||
use crate::h_stack;
|
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
|
||||||
pub trait ButtonCommon: Clickable + Disableable {
|
pub trait ButtonCommon: Clickable + Disableable {
|
||||||
@ -250,6 +249,7 @@ impl ButtonSize {
|
|||||||
/// This is also used to build the prebuilt buttons.
|
/// This is also used to build the prebuilt buttons.
|
||||||
#[derive(IntoElement)]
|
#[derive(IntoElement)]
|
||||||
pub struct ButtonLike {
|
pub struct ButtonLike {
|
||||||
|
base: Div,
|
||||||
id: ElementId,
|
id: ElementId,
|
||||||
pub(super) style: ButtonStyle,
|
pub(super) style: ButtonStyle,
|
||||||
pub(super) disabled: bool,
|
pub(super) disabled: bool,
|
||||||
@ -264,6 +264,7 @@ pub struct ButtonLike {
|
|||||||
impl ButtonLike {
|
impl ButtonLike {
|
||||||
pub fn new(id: impl Into<ElementId>) -> Self {
|
pub fn new(id: impl Into<ElementId>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
base: div(),
|
||||||
id: id.into(),
|
id: id.into(),
|
||||||
style: ButtonStyle::default(),
|
style: ButtonStyle::default(),
|
||||||
disabled: false,
|
disabled: false,
|
||||||
@ -331,6 +332,13 @@ impl ButtonCommon for ButtonLike {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl VisibleOnHover for ButtonLike {
|
||||||
|
fn visible_on_hover(mut self, group_name: impl Into<SharedString>) -> Self {
|
||||||
|
self.base = self.base.visible_on_hover(group_name);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl ParentElement for ButtonLike {
|
impl ParentElement for ButtonLike {
|
||||||
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
|
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
|
||||||
&mut self.children
|
&mut self.children
|
||||||
@ -341,7 +349,8 @@ impl RenderOnce for ButtonLike {
|
|||||||
type Rendered = Stateful<Div>;
|
type Rendered = Stateful<Div>;
|
||||||
|
|
||||||
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
||||||
h_stack()
|
self.base
|
||||||
|
.h_flex()
|
||||||
.id(self.id.clone())
|
.id(self.id.clone())
|
||||||
.group("")
|
.group("")
|
||||||
.flex_none()
|
.flex_none()
|
||||||
|
@ -98,6 +98,13 @@ impl ButtonCommon for IconButton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl VisibleOnHover for IconButton {
|
||||||
|
fn visible_on_hover(mut self, group_name: impl Into<SharedString>) -> Self {
|
||||||
|
self.base = self.base.visible_on_hover(group_name);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl RenderOnce for IconButton {
|
impl RenderOnce for IconButton {
|
||||||
type Rendered = ButtonLike;
|
type Rendered = ButtonLike;
|
||||||
|
|
||||||
|
@ -255,6 +255,9 @@ impl Render for ContextMenu {
|
|||||||
};
|
};
|
||||||
|
|
||||||
ListItem::new(label.clone())
|
ListItem::new(label.clone())
|
||||||
|
.inset(true)
|
||||||
|
.selected(Some(ix) == self.selected_index)
|
||||||
|
.on_click(move |_, cx| handler(cx))
|
||||||
.child(
|
.child(
|
||||||
h_stack()
|
h_stack()
|
||||||
.w_full()
|
.w_full()
|
||||||
@ -265,8 +268,6 @@ impl Render for ContextMenu {
|
|||||||
.map(|binding| div().ml_1().child(binding))
|
.map(|binding| div().ml_1().child(binding))
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
.selected(Some(ix) == self.selected_index)
|
|
||||||
.on_click(move |_, cx| handler(cx))
|
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
use crate::{prelude::*, Color, Icon, IconButton, IconSize};
|
|
||||||
use gpui::ClickEvent;
|
use gpui::ClickEvent;
|
||||||
|
|
||||||
|
use crate::{prelude::*, Color, Icon, IconButton, IconSize};
|
||||||
|
|
||||||
#[derive(IntoElement)]
|
#[derive(IntoElement)]
|
||||||
pub struct Disclosure {
|
pub struct Disclosure {
|
||||||
is_open: bool,
|
is_open: bool,
|
||||||
|
@ -1,12 +1,18 @@
|
|||||||
use crate::{h_stack, prelude::*, Disclosure, Icon, IconElement, IconSize, Label};
|
use crate::{h_stack, prelude::*, Disclosure, Label};
|
||||||
use gpui::{AnyElement, ClickEvent, Div};
|
use gpui::{AnyElement, ClickEvent, Div};
|
||||||
use smallvec::SmallVec;
|
|
||||||
|
|
||||||
#[derive(IntoElement)]
|
#[derive(IntoElement)]
|
||||||
pub struct ListHeader {
|
pub struct ListHeader {
|
||||||
|
/// The label of the header.
|
||||||
label: SharedString,
|
label: SharedString,
|
||||||
left_icon: Option<Icon>,
|
/// A slot for content that appears before the label, like an icon or avatar.
|
||||||
meta: SmallVec<[AnyElement; 2]>,
|
start_slot: Option<AnyElement>,
|
||||||
|
/// A slot for content that appears after the label, usually on the other side of the header.
|
||||||
|
/// This might be a button, a disclosure arrow, a face pile, etc.
|
||||||
|
end_slot: Option<AnyElement>,
|
||||||
|
/// A slot for content that appears on hover after the label
|
||||||
|
/// It will obscure the `end_slot` when visible.
|
||||||
|
end_hover_slot: Option<AnyElement>,
|
||||||
toggle: Option<bool>,
|
toggle: Option<bool>,
|
||||||
on_toggle: Option<Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
|
on_toggle: Option<Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
|
||||||
inset: bool,
|
inset: bool,
|
||||||
@ -17,8 +23,9 @@ impl ListHeader {
|
|||||||
pub fn new(label: impl Into<SharedString>) -> Self {
|
pub fn new(label: impl Into<SharedString>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
label: label.into(),
|
label: label.into(),
|
||||||
left_icon: None,
|
start_slot: None,
|
||||||
meta: SmallVec::new(),
|
end_slot: None,
|
||||||
|
end_hover_slot: None,
|
||||||
inset: false,
|
inset: false,
|
||||||
toggle: None,
|
toggle: None,
|
||||||
on_toggle: None,
|
on_toggle: None,
|
||||||
@ -39,13 +46,23 @@ impl ListHeader {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn left_icon(mut self, left_icon: impl Into<Option<Icon>>) -> Self {
|
pub fn start_slot<E: IntoElement>(mut self, start_slot: impl Into<Option<E>>) -> Self {
|
||||||
self.left_icon = left_icon.into();
|
self.start_slot = start_slot.into().map(IntoElement::into_any_element);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn meta(mut self, meta: impl IntoElement) -> Self {
|
pub fn end_slot<E: IntoElement>(mut self, end_slot: impl Into<Option<E>>) -> Self {
|
||||||
self.meta.push(meta.into_any_element());
|
self.end_slot = end_slot.into().map(IntoElement::into_any_element);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn end_hover_slot<E: IntoElement>(mut self, end_hover_slot: impl Into<Option<E>>) -> Self {
|
||||||
|
self.end_hover_slot = end_hover_slot.into().map(IntoElement::into_any_element);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn inset(mut self, inset: bool) -> Self {
|
||||||
|
self.inset = inset;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -61,9 +78,9 @@ impl RenderOnce for ListHeader {
|
|||||||
type Rendered = Div;
|
type Rendered = Div;
|
||||||
|
|
||||||
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
||||||
h_stack().w_full().relative().child(
|
h_stack().w_full().relative().group("list_header").child(
|
||||||
div()
|
div()
|
||||||
.h_5()
|
.h_7()
|
||||||
.when(self.inset, |this| this.px_2())
|
.when(self.inset, |this| this.px_2())
|
||||||
.when(self.selected, |this| {
|
.when(self.selected, |this| {
|
||||||
this.bg(cx.theme().colors().ghost_element_selected)
|
this.bg(cx.theme().colors().ghost_element_selected)
|
||||||
@ -77,24 +94,29 @@ impl RenderOnce for ListHeader {
|
|||||||
.child(
|
.child(
|
||||||
h_stack()
|
h_stack()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
|
.children(
|
||||||
|
self.toggle
|
||||||
|
.map(|is_open| Disclosure::new(is_open).on_toggle(self.on_toggle)),
|
||||||
|
)
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.flex()
|
.flex()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.items_center()
|
.items_center()
|
||||||
.children(self.left_icon.map(|i| {
|
.children(self.start_slot)
|
||||||
IconElement::new(i)
|
|
||||||
.color(Color::Muted)
|
|
||||||
.size(IconSize::Small)
|
|
||||||
}))
|
|
||||||
.child(Label::new(self.label.clone()).color(Color::Muted)),
|
.child(Label::new(self.label.clone()).color(Color::Muted)),
|
||||||
)
|
|
||||||
.children(
|
|
||||||
self.toggle
|
|
||||||
.map(|is_open| Disclosure::new(is_open).on_toggle(self.on_toggle)),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.child(h_stack().gap_2().items_center().children(self.meta)),
|
.child(h_stack().children(self.end_slot))
|
||||||
|
.when_some(self.end_hover_slot, |this, end_hover_slot| {
|
||||||
|
this.child(
|
||||||
|
div()
|
||||||
|
.absolute()
|
||||||
|
.right_0()
|
||||||
|
.visible_on_hover("list_header")
|
||||||
|
.child(end_hover_slot),
|
||||||
|
)
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,19 +1,24 @@
|
|||||||
use crate::{prelude::*, Avatar, Disclosure, Icon, IconElement, IconSize};
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
px, AnyElement, AnyView, ClickEvent, Div, ImageSource, MouseButton, MouseDownEvent, Pixels,
|
px, AnyElement, AnyView, ClickEvent, Div, MouseButton, MouseDownEvent, Pixels, Stateful,
|
||||||
Stateful,
|
|
||||||
};
|
};
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
|
|
||||||
|
use crate::{prelude::*, Disclosure};
|
||||||
|
|
||||||
#[derive(IntoElement)]
|
#[derive(IntoElement)]
|
||||||
pub struct ListItem {
|
pub struct ListItem {
|
||||||
id: ElementId,
|
id: ElementId,
|
||||||
selected: bool,
|
selected: bool,
|
||||||
// TODO: Reintroduce this
|
|
||||||
// disclosure_control_style: DisclosureControlVisibility,
|
|
||||||
indent_level: usize,
|
indent_level: usize,
|
||||||
indent_step_size: Pixels,
|
indent_step_size: Pixels,
|
||||||
left_slot: Option<AnyElement>,
|
/// A slot for content that appears before the children, like an icon or avatar.
|
||||||
|
start_slot: Option<AnyElement>,
|
||||||
|
/// A slot for content that appears after the children, usually on the other side of the header.
|
||||||
|
/// This might be a button, a disclosure arrow, a face pile, etc.
|
||||||
|
end_slot: Option<AnyElement>,
|
||||||
|
/// A slot for content that appears on hover after the children
|
||||||
|
/// It will obscure the `end_slot` when visible.
|
||||||
|
end_hover_slot: Option<AnyElement>,
|
||||||
toggle: Option<bool>,
|
toggle: Option<bool>,
|
||||||
inset: bool,
|
inset: bool,
|
||||||
on_click: Option<Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
|
on_click: Option<Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
|
||||||
@ -30,7 +35,9 @@ impl ListItem {
|
|||||||
selected: false,
|
selected: false,
|
||||||
indent_level: 0,
|
indent_level: 0,
|
||||||
indent_step_size: px(12.),
|
indent_step_size: px(12.),
|
||||||
left_slot: None,
|
start_slot: None,
|
||||||
|
end_slot: None,
|
||||||
|
end_hover_slot: None,
|
||||||
toggle: None,
|
toggle: None,
|
||||||
inset: false,
|
inset: false,
|
||||||
on_click: None,
|
on_click: None,
|
||||||
@ -87,23 +94,18 @@ impl ListItem {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn left_child(mut self, left_content: impl IntoElement) -> Self {
|
pub fn start_slot<E: IntoElement>(mut self, start_slot: impl Into<Option<E>>) -> Self {
|
||||||
self.left_slot = Some(left_content.into_any_element());
|
self.start_slot = start_slot.into().map(IntoElement::into_any_element);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn left_icon(mut self, left_icon: Icon) -> Self {
|
pub fn end_slot<E: IntoElement>(mut self, end_slot: impl Into<Option<E>>) -> Self {
|
||||||
self.left_slot = Some(
|
self.end_slot = end_slot.into().map(IntoElement::into_any_element);
|
||||||
IconElement::new(left_icon)
|
|
||||||
.size(IconSize::Small)
|
|
||||||
.color(Color::Muted)
|
|
||||||
.into_any_element(),
|
|
||||||
);
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn left_avatar(mut self, left_avatar: impl Into<ImageSource>) -> Self {
|
pub fn end_hover_slot<E: IntoElement>(mut self, end_hover_slot: impl Into<Option<E>>) -> Self {
|
||||||
self.left_slot = Some(Avatar::new(left_avatar).into_any_element());
|
self.end_hover_slot = end_hover_slot.into().map(IntoElement::into_any_element);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -125,49 +127,102 @@ impl RenderOnce for ListItem {
|
|||||||
type Rendered = Stateful<Div>;
|
type Rendered = Stateful<Div>;
|
||||||
|
|
||||||
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
||||||
div()
|
h_stack()
|
||||||
.id(self.id)
|
.id(self.id)
|
||||||
|
.w_full()
|
||||||
.relative()
|
.relative()
|
||||||
// TODO: Add focus state
|
// When an item is inset draw the indent spacing outside of the item
|
||||||
// .when(self.state == InteractionState::Focused, |this| {
|
.when(self.inset, |this| {
|
||||||
// this.border()
|
this.ml(self.indent_level as f32 * self.indent_step_size)
|
||||||
// .border_color(cx.theme().colors().border_focused)
|
.px_1()
|
||||||
// })
|
|
||||||
.when(self.inset, |this| this.rounded_md())
|
|
||||||
.hover(|style| style.bg(cx.theme().colors().ghost_element_hover))
|
|
||||||
.active(|style| style.bg(cx.theme().colors().ghost_element_active))
|
|
||||||
.when(self.selected, |this| {
|
|
||||||
this.bg(cx.theme().colors().ghost_element_selected)
|
|
||||||
})
|
})
|
||||||
.when_some(self.on_click, |this, on_click| {
|
.when(!self.inset, |this| {
|
||||||
this.cursor_pointer().on_click(move |event, cx| {
|
this
|
||||||
// HACK: GPUI currently fires `on_click` with any mouse button,
|
// TODO: Add focus state
|
||||||
// but we only care about the left button.
|
// .when(self.state == InteractionState::Focused, |this| {
|
||||||
if event.down.button == MouseButton::Left {
|
// this.border()
|
||||||
(on_click)(event, cx)
|
// .border_color(cx.theme().colors().border_focused)
|
||||||
}
|
// })
|
||||||
})
|
.hover(|style| style.bg(cx.theme().colors().ghost_element_hover))
|
||||||
|
.active(|style| style.bg(cx.theme().colors().ghost_element_active))
|
||||||
|
.when(self.selected, |this| {
|
||||||
|
this.bg(cx.theme().colors().ghost_element_selected)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.when_some(self.on_secondary_mouse_down, |this, on_mouse_down| {
|
|
||||||
this.on_mouse_down(MouseButton::Right, move |event, cx| {
|
|
||||||
(on_mouse_down)(event, cx)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.when_some(self.tooltip, |this, tooltip| this.tooltip(tooltip))
|
|
||||||
.child(
|
.child(
|
||||||
div()
|
h_stack()
|
||||||
.when(self.inset, |this| this.px_2())
|
.id("inner_list_item")
|
||||||
.ml(self.indent_level as f32 * self.indent_step_size)
|
.w_full()
|
||||||
.flex()
|
|
||||||
.gap_1()
|
|
||||||
.items_center()
|
|
||||||
.relative()
|
.relative()
|
||||||
.children(
|
.gap_1()
|
||||||
self.toggle
|
.px_2()
|
||||||
.map(|is_open| Disclosure::new(is_open).on_toggle(self.on_toggle)),
|
.group("list_item")
|
||||||
|
.when(self.inset, |this| {
|
||||||
|
this
|
||||||
|
// TODO: Add focus state
|
||||||
|
// .when(self.state == InteractionState::Focused, |this| {
|
||||||
|
// this.border()
|
||||||
|
// .border_color(cx.theme().colors().border_focused)
|
||||||
|
// })
|
||||||
|
.hover(|style| style.bg(cx.theme().colors().ghost_element_hover))
|
||||||
|
.active(|style| style.bg(cx.theme().colors().ghost_element_active))
|
||||||
|
.when(self.selected, |this| {
|
||||||
|
this.bg(cx.theme().colors().ghost_element_selected)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.when_some(self.on_click, |this, on_click| {
|
||||||
|
this.cursor_pointer().on_click(on_click)
|
||||||
|
})
|
||||||
|
.when_some(self.on_secondary_mouse_down, |this, on_mouse_down| {
|
||||||
|
this.on_mouse_down(MouseButton::Right, move |event, cx| {
|
||||||
|
(on_mouse_down)(event, cx)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.when_some(self.tooltip, |this, tooltip| this.tooltip(tooltip))
|
||||||
|
.map(|this| {
|
||||||
|
if self.inset {
|
||||||
|
this.rounded_md()
|
||||||
|
} else {
|
||||||
|
// When an item is not inset draw the indent spacing inside of the item
|
||||||
|
this.ml(self.indent_level as f32 * self.indent_step_size)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.children(self.toggle.map(|is_open| {
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.absolute()
|
||||||
|
.left(rems(-1.))
|
||||||
|
.visible_on_hover("")
|
||||||
|
.child(Disclosure::new(is_open).on_toggle(self.on_toggle))
|
||||||
|
}))
|
||||||
|
.child(
|
||||||
|
h_stack()
|
||||||
|
.flex_1()
|
||||||
|
.gap_1()
|
||||||
|
.children(self.start_slot)
|
||||||
|
.children(self.children),
|
||||||
)
|
)
|
||||||
.children(self.left_slot)
|
.when_some(self.end_slot, |this, end_slot| {
|
||||||
.children(self.children),
|
this.justify_between().child(
|
||||||
|
h_stack()
|
||||||
|
.when(self.end_hover_slot.is_some(), |this| {
|
||||||
|
this.visible()
|
||||||
|
.group_hover("list_item", |this| this.invisible())
|
||||||
|
})
|
||||||
|
.child(end_slot),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.when_some(self.end_hover_slot, |this, end_hover_slot| {
|
||||||
|
this.child(
|
||||||
|
h_stack()
|
||||||
|
.h_full()
|
||||||
|
.absolute()
|
||||||
|
.right_2()
|
||||||
|
.top_0()
|
||||||
|
.visible_on_hover("list_item")
|
||||||
|
.child(end_hover_slot),
|
||||||
|
)
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,10 @@ impl RenderOnce for ListSeparator {
|
|||||||
type Rendered = Div;
|
type Rendered = Div;
|
||||||
|
|
||||||
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
||||||
div().h_px().w_full().bg(cx.theme().colors().border_variant)
|
div()
|
||||||
|
.h_px()
|
||||||
|
.w_full()
|
||||||
|
.my_1()
|
||||||
|
.bg(cx.theme().colors().border_variant)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@ use crate::{h_stack, Icon, IconElement, IconSize, Label};
|
|||||||
#[derive(IntoElement)]
|
#[derive(IntoElement)]
|
||||||
pub struct ListSubHeader {
|
pub struct ListSubHeader {
|
||||||
label: SharedString,
|
label: SharedString,
|
||||||
left_icon: Option<Icon>,
|
start_slot: Option<Icon>,
|
||||||
inset: bool,
|
inset: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -14,13 +14,13 @@ impl ListSubHeader {
|
|||||||
pub fn new(label: impl Into<SharedString>) -> Self {
|
pub fn new(label: impl Into<SharedString>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
label: label.into(),
|
label: label.into(),
|
||||||
left_icon: None,
|
start_slot: None,
|
||||||
inset: false,
|
inset: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn left_icon(mut self, left_icon: Option<Icon>) -> Self {
|
pub fn left_icon(mut self, left_icon: Option<Icon>) -> Self {
|
||||||
self.left_icon = left_icon;
|
self.start_slot = left_icon;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -44,7 +44,7 @@ impl RenderOnce for ListSubHeader {
|
|||||||
.flex()
|
.flex()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.items_center()
|
.items_center()
|
||||||
.children(self.left_icon.map(|i| {
|
.children(self.start_slot.map(|i| {
|
||||||
IconElement::new(i)
|
IconElement::new(i)
|
||||||
.color(Color::Muted)
|
.color(Color::Muted)
|
||||||
.size(IconSize::Small)
|
.size(IconSize::Small)
|
||||||
|
@ -15,19 +15,19 @@ impl Render for ListHeaderStory {
|
|||||||
.child(Story::label("Default"))
|
.child(Story::label("Default"))
|
||||||
.child(ListHeader::new("Section 1"))
|
.child(ListHeader::new("Section 1"))
|
||||||
.child(Story::label("With left icon"))
|
.child(Story::label("With left icon"))
|
||||||
.child(ListHeader::new("Section 2").left_icon(Icon::Bell))
|
.child(ListHeader::new("Section 2").start_slot(IconElement::new(Icon::Bell)))
|
||||||
.child(Story::label("With left icon and meta"))
|
.child(Story::label("With left icon and meta"))
|
||||||
.child(
|
.child(
|
||||||
ListHeader::new("Section 3")
|
ListHeader::new("Section 3")
|
||||||
.left_icon(Icon::BellOff)
|
.start_slot(IconElement::new(Icon::BellOff))
|
||||||
.meta(IconButton::new("action_1", Icon::Bolt)),
|
.end_slot(IconButton::new("action_1", Icon::Bolt)),
|
||||||
)
|
)
|
||||||
.child(Story::label("With multiple meta"))
|
.child(Story::label("With multiple meta"))
|
||||||
.child(
|
.child(
|
||||||
ListHeader::new("Section 4")
|
ListHeader::new("Section 4")
|
||||||
.meta(IconButton::new("action_1", Icon::Bolt))
|
.end_slot(IconButton::new("action_1", Icon::Bolt))
|
||||||
.meta(IconButton::new("action_2", Icon::ExclamationTriangle))
|
.end_slot(IconButton::new("action_2", Icon::ExclamationTriangle))
|
||||||
.meta(IconButton::new("action_3", Icon::Plus)),
|
.end_slot(IconButton::new("action_3", Icon::Plus)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use gpui::{Div, Render};
|
use gpui::{Div, Render};
|
||||||
use story::Story;
|
use story::Story;
|
||||||
|
|
||||||
use crate::prelude::*;
|
use crate::{prelude::*, Avatar};
|
||||||
use crate::{Icon, ListItem};
|
use crate::{Icon, ListItem};
|
||||||
|
|
||||||
pub struct ListItemStory;
|
pub struct ListItemStory;
|
||||||
@ -9,24 +9,80 @@ pub struct ListItemStory;
|
|||||||
impl Render for ListItemStory {
|
impl Render for ListItemStory {
|
||||||
type Element = Div;
|
type Element = Div;
|
||||||
|
|
||||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||||
Story::container()
|
Story::container()
|
||||||
|
.bg(cx.theme().colors().background)
|
||||||
.child(Story::title_for::<ListItem>())
|
.child(Story::title_for::<ListItem>())
|
||||||
.child(Story::label("Default"))
|
.child(Story::label("Default"))
|
||||||
.child(ListItem::new("hello_world").child("Hello, world!"))
|
.child(ListItem::new("hello_world").child("Hello, world!"))
|
||||||
.child(Story::label("With left icon"))
|
.child(Story::label("Inset"))
|
||||||
.child(
|
.child(
|
||||||
ListItem::new("with_left_icon")
|
ListItem::new("inset_list_item")
|
||||||
|
.inset(true)
|
||||||
|
.start_slot(
|
||||||
|
IconElement::new(Icon::Bell)
|
||||||
|
.size(IconSize::Small)
|
||||||
|
.color(Color::Muted),
|
||||||
|
)
|
||||||
.child("Hello, world!")
|
.child("Hello, world!")
|
||||||
.left_icon(Icon::Bell),
|
.end_slot(
|
||||||
|
IconElement::new(Icon::Bell)
|
||||||
|
.size(IconSize::Small)
|
||||||
|
.color(Color::Muted),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.child(Story::label("With left avatar"))
|
.child(Story::label("With start slot icon"))
|
||||||
|
.child(
|
||||||
|
ListItem::new("with start slot_icon")
|
||||||
|
.child("Hello, world!")
|
||||||
|
.start_slot(
|
||||||
|
IconElement::new(Icon::Bell)
|
||||||
|
.size(IconSize::Small)
|
||||||
|
.color(Color::Muted),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(Story::label("With start slot avatar"))
|
||||||
|
.child(
|
||||||
|
ListItem::new("with_start slot avatar")
|
||||||
|
.child("Hello, world!")
|
||||||
|
.start_slot(Avatar::new(SharedString::from(
|
||||||
|
"https://avatars.githubusercontent.com/u/1714999?v=4",
|
||||||
|
))),
|
||||||
|
)
|
||||||
|
.child(Story::label("With end slot"))
|
||||||
.child(
|
.child(
|
||||||
ListItem::new("with_left_avatar")
|
ListItem::new("with_left_avatar")
|
||||||
.child("Hello, world!")
|
.child("Hello, world!")
|
||||||
.left_avatar(SharedString::from(
|
.end_slot(Avatar::new(SharedString::from(
|
||||||
"https://avatars.githubusercontent.com/u/1714999?v=4",
|
"https://avatars.githubusercontent.com/u/1714999?v=4",
|
||||||
)),
|
))),
|
||||||
|
)
|
||||||
|
.child(Story::label("With end hover slot"))
|
||||||
|
.child(
|
||||||
|
ListItem::new("with_end_hover_slot")
|
||||||
|
.child("Hello, world!")
|
||||||
|
.end_slot(
|
||||||
|
h_stack()
|
||||||
|
.gap_2()
|
||||||
|
.child(Avatar::new(SharedString::from(
|
||||||
|
"https://avatars.githubusercontent.com/u/1789?v=4",
|
||||||
|
)))
|
||||||
|
.child(Avatar::new(SharedString::from(
|
||||||
|
"https://avatars.githubusercontent.com/u/1789?v=4",
|
||||||
|
)))
|
||||||
|
.child(Avatar::new(SharedString::from(
|
||||||
|
"https://avatars.githubusercontent.com/u/1789?v=4",
|
||||||
|
)))
|
||||||
|
.child(Avatar::new(SharedString::from(
|
||||||
|
"https://avatars.githubusercontent.com/u/1789?v=4",
|
||||||
|
)))
|
||||||
|
.child(Avatar::new(SharedString::from(
|
||||||
|
"https://avatars.githubusercontent.com/u/1789?v=4",
|
||||||
|
))),
|
||||||
|
)
|
||||||
|
.end_hover_slot(Avatar::new(SharedString::from(
|
||||||
|
"https://avatars.githubusercontent.com/u/1714999?v=4",
|
||||||
|
))),
|
||||||
)
|
)
|
||||||
.child(Story::label("With `on_click`"))
|
.child(Story::label("With `on_click`"))
|
||||||
.child(
|
.child(
|
||||||
|
@ -158,7 +158,6 @@ impl RenderOnce for Tab {
|
|||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
h_stack()
|
h_stack()
|
||||||
.invisible()
|
|
||||||
.w_3()
|
.w_3()
|
||||||
.h_3()
|
.h_3()
|
||||||
.justify_center()
|
.justify_center()
|
||||||
@ -167,7 +166,7 @@ impl RenderOnce for Tab {
|
|||||||
TabCloseSide::Start => this.left_1(),
|
TabCloseSide::Start => this.left_1(),
|
||||||
TabCloseSide::End => this.right_1(),
|
TabCloseSide::End => this.right_1(),
|
||||||
})
|
})
|
||||||
.group_hover("", |style| style.visible())
|
.visible_on_hover("")
|
||||||
.children(self.end_slot),
|
.children(self.end_slot),
|
||||||
)
|
)
|
||||||
.children(self.children),
|
.children(self.children),
|
||||||
|
@ -9,6 +9,7 @@ pub use crate::clickable::*;
|
|||||||
pub use crate::disableable::*;
|
pub use crate::disableable::*;
|
||||||
pub use crate::fixed::*;
|
pub use crate::fixed::*;
|
||||||
pub use crate::selectable::*;
|
pub use crate::selectable::*;
|
||||||
|
pub use crate::visible_on_hover::*;
|
||||||
pub use crate::{h_stack, v_stack};
|
pub use crate::{h_stack, v_stack};
|
||||||
pub use crate::{Button, ButtonSize, ButtonStyle, IconButton};
|
pub use crate::{Button, ButtonSize, ButtonStyle, IconButton};
|
||||||
pub use crate::{ButtonCommon, Color, StyledExt};
|
pub use crate::{ButtonCommon, Color, StyledExt};
|
||||||
|
@ -118,16 +118,26 @@ pub trait StyledExt: Styled + Sized {
|
|||||||
elevated(self, cx, ElevationIndex::ModalSurface)
|
elevated(self, cx, ElevationIndex::ModalSurface)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The theme's primary border color.
|
||||||
|
fn border_primary(self, cx: &mut WindowContext) -> Self {
|
||||||
|
self.border_color(cx.theme().colors().border)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The theme's secondary or muted border color.
|
||||||
|
fn border_muted(self, cx: &mut WindowContext) -> Self {
|
||||||
|
self.border_color(cx.theme().colors().border_variant)
|
||||||
|
}
|
||||||
|
|
||||||
fn debug_bg_red(self) -> Self {
|
fn debug_bg_red(self) -> Self {
|
||||||
self.bg(gpui::red())
|
self.bg(hsla(0. / 360., 1., 0.5, 1.))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn debug_bg_green(self) -> Self {
|
fn debug_bg_green(self) -> Self {
|
||||||
self.bg(gpui::green())
|
self.bg(hsla(120. / 360., 1., 0.5, 1.))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn debug_bg_blue(self) -> Self {
|
fn debug_bg_blue(self) -> Self {
|
||||||
self.bg(gpui::blue())
|
self.bg(hsla(240. / 360., 1., 0.5, 1.))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn debug_bg_yellow(self) -> Self {
|
fn debug_bg_yellow(self) -> Self {
|
||||||
|
@ -21,6 +21,7 @@ mod selectable;
|
|||||||
mod styled_ext;
|
mod styled_ext;
|
||||||
mod styles;
|
mod styles;
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
|
mod visible_on_hover;
|
||||||
|
|
||||||
pub use clickable::*;
|
pub use clickable::*;
|
||||||
pub use components::*;
|
pub use components::*;
|
||||||
@ -30,3 +31,4 @@ pub use prelude::*;
|
|||||||
pub use selectable::*;
|
pub use selectable::*;
|
||||||
pub use styled_ext::*;
|
pub use styled_ext::*;
|
||||||
pub use styles::*;
|
pub use styles::*;
|
||||||
|
pub use visible_on_hover::*;
|
||||||
|
15
crates/ui2/src/visible_on_hover.rs
Normal file
15
crates/ui2/src/visible_on_hover.rs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
use gpui::{InteractiveElement, SharedString, Styled};
|
||||||
|
|
||||||
|
pub trait VisibleOnHover {
|
||||||
|
/// Sets the element to only be visible when the specified group is hovered.
|
||||||
|
///
|
||||||
|
/// Pass `""` as the `group_name` to use the global group.
|
||||||
|
fn visible_on_hover(self, group_name: impl Into<SharedString>) -> Self;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E: InteractiveElement + Styled> VisibleOnHover for E {
|
||||||
|
fn visible_on_hover(self, group_name: impl Into<SharedString>) -> Self {
|
||||||
|
self.invisible()
|
||||||
|
.group_hover(group_name, |style| style.visible())
|
||||||
|
}
|
||||||
|
}
|
@ -1759,6 +1759,34 @@ impl Pane {
|
|||||||
})
|
})
|
||||||
.log_err();
|
.log_err();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn handle_split_tab_drop(
|
||||||
|
&mut self,
|
||||||
|
dragged_tab: &View<DraggedTab>,
|
||||||
|
split_direction: SplitDirection,
|
||||||
|
cx: &mut ViewContext<'_, Pane>,
|
||||||
|
) {
|
||||||
|
let dragged_tab = dragged_tab.read(cx);
|
||||||
|
let item_id = dragged_tab.item_id;
|
||||||
|
let from_pane = dragged_tab.pane.clone();
|
||||||
|
let to_pane = cx.view().clone();
|
||||||
|
self.workspace
|
||||||
|
.update(cx, |workspace, cx| {
|
||||||
|
cx.defer(move |workspace, cx| {
|
||||||
|
let item = from_pane
|
||||||
|
.read(cx)
|
||||||
|
.items()
|
||||||
|
.find(|item| item.item_id() == item_id)
|
||||||
|
.map(|item| item.boxed_clone());
|
||||||
|
if let Some(item) = item {
|
||||||
|
if let Some(item) = item.clone_on_split(workspace.database_id(), cx) {
|
||||||
|
workspace.split_item(split_direction, item, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.log_err();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FocusableView for Pane {
|
impl FocusableView for Pane {
|
||||||
@ -1852,7 +1880,54 @@ impl Render for Pane {
|
|||||||
.child(self.render_tab_bar(cx))
|
.child(self.render_tab_bar(cx))
|
||||||
.child(self.toolbar.clone())
|
.child(self.toolbar.clone())
|
||||||
.child(if let Some(item) = self.active_item() {
|
.child(if let Some(item) = self.active_item() {
|
||||||
div().flex().flex_1().child(item.to_any())
|
let mut drag_target_color = cx.theme().colors().text;
|
||||||
|
drag_target_color.a = 0.5;
|
||||||
|
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.flex_1()
|
||||||
|
.relative()
|
||||||
|
.child(item.to_any())
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.absolute()
|
||||||
|
.full()
|
||||||
|
.z_index(1)
|
||||||
|
.drag_over::<DraggedTab>(|style| style.bg(drag_target_color))
|
||||||
|
.on_drop(cx.listener(
|
||||||
|
move |this, dragged_tab: &View<DraggedTab>, cx| {
|
||||||
|
this.handle_tab_drop(dragged_tab, this.active_item_index(), cx)
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.children(
|
||||||
|
[
|
||||||
|
(SplitDirection::Up, 2),
|
||||||
|
(SplitDirection::Down, 2),
|
||||||
|
(SplitDirection::Left, 3),
|
||||||
|
(SplitDirection::Right, 3),
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.map(|(direction, z_index)| {
|
||||||
|
let div = div()
|
||||||
|
.absolute()
|
||||||
|
.z_index(z_index)
|
||||||
|
.invisible()
|
||||||
|
.bg(drag_target_color)
|
||||||
|
.drag_over::<DraggedTab>(|style| style.visible())
|
||||||
|
.on_drop(cx.listener(
|
||||||
|
move |this, dragged_tab: &View<DraggedTab>, cx| {
|
||||||
|
this.handle_split_tab_drop(dragged_tab, direction, cx)
|
||||||
|
},
|
||||||
|
));
|
||||||
|
match direction {
|
||||||
|
SplitDirection::Up => div.top_0().left_0().right_0().h_32(),
|
||||||
|
SplitDirection::Down => div.left_0().bottom_0().right_0().h_32(),
|
||||||
|
SplitDirection::Left => div.top_0().left_0().bottom_0().w_32(),
|
||||||
|
SplitDirection::Right => div.top_0().bottom_0().right_0().w_32(),
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
h_stack()
|
h_stack()
|
||||||
.items_center()
|
.items_center()
|
||||||
|
@ -55,7 +55,7 @@ outline = { package = "outline2", path = "../outline2" }
|
|||||||
# plugin_runtime = { path = "../plugin_runtime",optional = true }
|
# plugin_runtime = { path = "../plugin_runtime",optional = true }
|
||||||
project = { package = "project2", path = "../project2" }
|
project = { package = "project2", path = "../project2" }
|
||||||
project_panel = { package = "project_panel2", path = "../project_panel2" }
|
project_panel = { package = "project_panel2", path = "../project_panel2" }
|
||||||
# project_symbols = { path = "../project_symbols" }
|
project_symbols = { package = "project_symbols2", path = "../project_symbols2" }
|
||||||
quick_action_bar = { package = "quick_action_bar2", path = "../quick_action_bar2" }
|
quick_action_bar = { package = "quick_action_bar2", path = "../quick_action_bar2" }
|
||||||
recent_projects = { package = "recent_projects2", path = "../recent_projects2" }
|
recent_projects = { package = "recent_projects2", path = "../recent_projects2" }
|
||||||
rope = { package = "rope2", path = "../rope2"}
|
rope = { package = "rope2", path = "../rope2"}
|
||||||
|
@ -205,7 +205,7 @@ fn main() {
|
|||||||
go_to_line::init(cx);
|
go_to_line::init(cx);
|
||||||
file_finder::init(cx);
|
file_finder::init(cx);
|
||||||
outline::init(cx);
|
outline::init(cx);
|
||||||
// project_symbols::init(cx);
|
project_symbols::init(cx);
|
||||||
project_panel::init(Assets, cx);
|
project_panel::init(Assets, cx);
|
||||||
channel::init(&client, user_store.clone(), cx);
|
channel::init(&client, user_store.clone(), cx);
|
||||||
// diagnostics::init(cx);
|
// diagnostics::init(cx);
|
||||||
|
1
docker-compose.sql
Normal file
1
docker-compose.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
create database zed;
|
16
docker-compose.yml
Normal file
16
docker-compose.yml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
version: "3.7"
|
||||||
|
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:15
|
||||||
|
container_name: zed_postgres
|
||||||
|
ports:
|
||||||
|
- 5432:5432
|
||||||
|
environment:
|
||||||
|
POSTGRES_HOST_AUTH_METHOD: trust
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
- ./docker-compose.sql:/docker-entrypoint-initdb.d/init.sql
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
15
script/storybook
Executable file
15
script/storybook
Executable file
@ -0,0 +1,15 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# This script takes a single text input and replaces 'list_item' with the input in a cargo run command
|
||||||
|
|
||||||
|
# Check if an argument is provided
|
||||||
|
if [ "$#" -ne 1 ]; then
|
||||||
|
echo "Usage: $0 <component_name>"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Assign the argument to a variable
|
||||||
|
COMPONENT_NAME="$1"
|
||||||
|
|
||||||
|
# Run the cargo command with the provided component name
|
||||||
|
cargo run -p storybook2 -- components/"$COMPONENT_NAME"
|
Loading…
Reference in New Issue
Block a user