Merge branch 'main' into fix-splits-bugs

This commit is contained in:
Mikayla 2023-12-13 18:29:48 -08:00
commit d88fc27b79
No known key found for this signature in database
33 changed files with 1130 additions and 350 deletions

25
Cargo.lock generated
View File

@ -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",

View File

@ -90,6 +90,7 @@ members = [
"crates/project_panel", "crates/project_panel",
"crates/project_panel2", "crates/project_panel2",
"crates/project_symbols", "crates/project_symbols",
"crates/project_symbols2",
"crates/quick_action_bar2", "crates/quick_action_bar2",
"crates/recent_projects", "crates/recent_projects",
"crates/recent_projects2", "crates/recent_projects2",

View File

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

View File

@ -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

View File

@ -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()

View File

@ -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)
} }

View File

@ -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
} }

View File

@ -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 {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

1
docker-compose.sql Normal file
View File

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

16
docker-compose.yml Normal file
View File

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

15
script/storybook Executable file
View 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"