Detailed changes
@@ -0,0 +1,6 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M2.25 4.80555V3.52777C2.25 3.18889 2.38462 2.86388 2.62425 2.62425C2.86388 2.38462 3.18889 2.25 3.52777 2.25H4.80555" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M11.1945 2.25H12.4722C12.8111 2.25 13.1361 2.38462 13.3758 2.62425C13.6154 2.86388 13.75 3.18889 13.75 3.52777V4.80555" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M13.75 11.1945V12.4722C13.75 12.8111 13.6154 13.1361 13.3758 13.3758C13.1361 13.6154 12.8111 13.75 12.4722 13.75H11.1945" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M4.80555 13.75H3.52777C3.18889 13.75 2.86388 13.6154 2.62425 13.3758C2.38462 13.1361 2.25 12.8111 2.25 12.4722V11.1945" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>
@@ -650,7 +650,7 @@ impl PickerDelegate for ConfigOptionPickerDelegate {
.end_slot(div().pr_2().when(is_selected, |this| {
this.child(Icon::new(IconName::Check).color(Color::Accent))
}))
- .end_hover_slot(div().pr_1p5().child({
+ .end_slot_on_hover(div().pr_1p5().child({
let (icon, color, tooltip) = if is_favorite {
(IconName::StarFilled, Color::Accent, "Unfavorite")
} else {
@@ -160,7 +160,7 @@ impl RenderOnce for ModelSelectorListItem {
.end_slot(div().pr_2().when(self.is_selected, |this| {
this.child(Icon::new(IconName::Check).color(Color::Accent))
}))
- .end_hover_slot(div().pr_1p5().when_some(self.on_toggle_favorite, {
+ .end_slot_on_hover(div().pr_1p5().when_some(self.on_toggle_favorite, {
|this, handle_click| {
let (icon, color, tooltip) = if is_favorite {
(IconName::StarFilled, Color::Accent, "Unfavorite Model")
@@ -1087,13 +1087,8 @@ impl PickerDelegate for BranchListDelegate {
),
)
.when(!is_new_items && !is_head_branch, |this| {
- this.map(|this| {
- if self.selected_index() == ix {
- this.end_slot(deleted_branch_icon(ix))
- } else {
- this.end_hover_slot(deleted_branch_icon(ix))
- }
- })
+ this.end_slot(deleted_branch_icon(ix))
+ .show_end_slot_on_hover()
})
.when_some(
if is_new_items {
@@ -1102,13 +1097,8 @@ impl PickerDelegate for BranchListDelegate {
None
},
|this, create_from_default_button| {
- this.map(|this| {
- if self.selected_index() == ix {
- this.end_slot(create_from_default_button)
- } else {
- this.end_hover_slot(create_from_default_button)
- }
- })
+ this.end_slot(create_from_default_button)
+ .show_end_slot_on_hover()
},
),
)
@@ -501,16 +501,39 @@ impl PickerDelegate for StashListDelegate {
.size(LabelSize::Small),
);
- let focus_handle = self.focus_handle.clone();
+ let view_button = {
+ let focus_handle = self.focus_handle.clone();
+ IconButton::new(("view-stash", ix), IconName::Eye)
+ .icon_size(IconSize::Small)
+ .tooltip(move |_, cx| {
+ Tooltip::for_action_in("View Stash", &ShowStashItem, &focus_handle, cx)
+ })
+ .on_click(cx.listener(move |this, _, window, cx| {
+ this.delegate.show_stash_at(ix, window, cx);
+ }))
+ };
+
+ let pop_button = {
+ let focus_handle = self.focus_handle.clone();
+ IconButton::new(("pop-stash", ix), IconName::MaximizeAlt)
+ .icon_size(IconSize::Small)
+ .tooltip(move |_, cx| {
+ Tooltip::for_action_in("Pop Stash", &menu::SecondaryConfirm, &focus_handle, cx)
+ })
+ .on_click(|_, window, cx| {
+ window.dispatch_action(menu::SecondaryConfirm.boxed_clone(), cx);
+ })
+ };
- let drop_button = |entry_ix: usize| {
- IconButton::new(("drop-stash", entry_ix), IconName::Trash)
+ let drop_button = {
+ let focus_handle = self.focus_handle.clone();
+ IconButton::new(("drop-stash", ix), IconName::Trash)
.icon_size(IconSize::Small)
.tooltip(move |_, cx| {
Tooltip::for_action_in("Drop Stash", &DropStashItem, &focus_handle, cx)
})
.on_click(cx.listener(move |this, _, window, cx| {
- this.delegate.drop_stash_at(entry_ix, window, cx);
+ this.delegate.drop_stash_at(ix, window, cx);
}))
};
@@ -530,17 +553,14 @@ impl PickerDelegate for StashListDelegate {
)
.child(div().w_full().child(stash_label).child(branch_info)),
)
- .tooltip(Tooltip::text(format!(
- "stash@{{{}}}",
- entry_match.entry.index
- )))
- .map(|this| {
- if selected {
- this.end_slot(drop_button(ix))
- } else {
- this.end_hover_slot(drop_button(ix))
- }
- }),
+ .end_slot(
+ h_flex()
+ .gap_0p5()
+ .child(view_button)
+ .child(pop_button)
+ .child(drop_button),
+ )
+ .show_end_slot_on_hover(),
)
}
@@ -549,6 +569,10 @@ impl PickerDelegate for StashListDelegate {
}
fn render_footer(&self, _: &mut Window, cx: &mut Context<Picker<Self>>) -> Option<AnyElement> {
+ if self.matches.is_empty() {
+ return None;
+ }
+
let focus_handle = self.focus_handle.clone();
Some(
@@ -884,12 +884,30 @@ impl PickerDelegate for WorktreeListDelegate {
}
})),
)
- .when(can_delete, |this| {
- if selected {
- this.end_slot(delete_button(ix))
- } else {
- this.end_hover_slot(delete_button(ix))
- }
+ .when(!entry.is_new, |this| {
+ let focus_handle = self.focus_handle.clone();
+ let open_in_new_window_button =
+ IconButton::new(("open-new-window", ix), IconName::ArrowUpRight)
+ .icon_size(IconSize::Small)
+ .tooltip(move |_, cx| {
+ Tooltip::for_action_in(
+ "Open in New Window",
+ &menu::SecondaryConfirm,
+ &focus_handle,
+ cx,
+ )
+ })
+ .on_click(|_, window, cx| {
+ window.dispatch_action(menu::SecondaryConfirm.boxed_clone(), cx);
+ });
+
+ this.end_slot(
+ h_flex()
+ .gap_0p5()
+ .child(open_in_new_window_button)
+ .when(can_delete, |this| this.child(delete_button(ix))),
+ )
+ .show_end_slot_on_hover()
}),
)
}
@@ -174,6 +174,7 @@ pub enum IconName {
LockOutlined,
MagnifyingGlass,
Maximize,
+ MaximizeAlt,
Menu,
MenuAltTemp,
Mic,
@@ -1264,13 +1264,8 @@ impl PickerDelegate for RecentProjectsDelegate {
this.tooltip(Tooltip::text(path.to_string_lossy().to_string()))
}),
)
- .map(|el| {
- if self.selected_index == ix {
- el.end_slot(secondary_actions)
- } else {
- el.end_hover_slot(secondary_actions)
- }
- })
+ .end_slot(secondary_actions)
+ .show_end_slot_on_hover()
.into_any_element(),
)
}
@@ -1363,13 +1358,8 @@ impl PickerDelegate for RecentProjectsDelegate {
})
.tooltip(Tooltip::text(tooltip_path)),
)
- .map(|el| {
- if self.selected_index == ix {
- el.end_slot(secondary_actions)
- } else {
- el.end_hover_slot(secondary_actions)
- }
- })
+ .end_slot(secondary_actions)
+ .show_end_slot_on_hover()
.into_any_element(),
)
}
@@ -1503,13 +1493,8 @@ impl PickerDelegate for RecentProjectsDelegate {
})
.tooltip(Tooltip::text(tooltip_path)),
)
- .map(|el| {
- if self.selected_index == ix {
- el.end_slot(secondary_actions)
- } else {
- el.end_hover_slot(secondary_actions)
- }
- })
+ .end_slot(secondary_actions)
+ .show_end_slot_on_hover()
.into_any_element(),
)
}
@@ -1621,23 +1621,24 @@ impl RemoteServerProjects {
}))
.tooltip(Tooltip::text(project.paths.join("\n")))
.when(is_from_zed, |server_list_item| {
- server_list_item.end_hover_slot::<AnyElement>(Some(
- div()
- .mr_2()
- .child({
- let project = project.clone();
- // Right-margin to offset it from the Scrollbar
- IconButton::new("remove-remote-project", IconName::Trash)
- .icon_size(IconSize::Small)
- .shape(IconButtonShape::Square)
- .size(ButtonSize::Large)
- .tooltip(Tooltip::text("Delete Remote Project"))
- .on_click(cx.listener(move |this, _, _, cx| {
- this.delete_remote_project(server_ix, &project, cx)
- }))
- })
- .into_any_element(),
- ))
+ server_list_item
+ .end_slot(
+ div()
+ .mr_2()
+ .child({
+ let project = project.clone();
+ IconButton::new("remove-remote-project", IconName::Trash)
+ .icon_size(IconSize::Small)
+ .shape(IconButtonShape::Square)
+ .size(ButtonSize::Large)
+ .tooltip(Tooltip::text("Delete Remote Project"))
+ .on_click(cx.listener(move |this, _, _, cx| {
+ this.delete_remote_project(server_ix, &project, cx)
+ }))
+ })
+ .into_any_element(),
+ )
+ .show_end_slot_on_hover()
}),
)
}
@@ -2413,9 +2414,8 @@ impl RemoteServerProjects {
.spacing(ui::ListItemSpacing::Sparse)
.start_slot(Icon::new(IconName::Copy).color(Color::Muted))
.child(Label::new("Copy Server Address"))
- .end_hover_slot(
- Label::new(connection_string.clone()).color(Color::Muted),
- )
+ .end_slot(Label::new(connection_string.clone()).color(Color::Muted))
+ .show_end_slot_on_hover()
.on_click({
let connection_string = connection_string.clone();
move |_, _, cx| {
@@ -389,7 +389,7 @@ impl PickerDelegate for RulePickerDelegate {
}))
}))
.when(!prompt_id.is_built_in(), |this| {
- this.end_hover_slot(
+ this.end_slot_on_hover(
h_flex()
.child(
IconButton::new("delete-rule", IconName::Trash)
@@ -875,7 +875,7 @@ impl PickerDelegate for TabSwitcherDelegate {
el.end_slot::<AnyElement>(close_button)
} else {
el.end_slot::<AnyElement>(indicator)
- .end_hover_slot::<AnyElement>(close_button)
+ .end_slot_on_hover::<AnyElement>(close_button)
}
}),
)
@@ -570,7 +570,7 @@ impl PickerDelegate for TasksModalDelegate {
Tooltip::simple("Delete Previously Scheduled Task", cx)
}),
);
- item.end_hover_slot(delete_button)
+ item.end_slot_on_hover(delete_button)
} else {
item
}
@@ -14,6 +14,14 @@ pub enum ListItemSpacing {
Sparse,
}
+#[derive(Default)]
+enum EndSlotVisibility {
+ #[default]
+ Always,
+ OnHover,
+ SwapOnHover(AnyElement),
+}
+
#[derive(IntoElement, RegisterComponent)]
pub struct ListItem {
id: ElementId,
@@ -28,9 +36,7 @@ pub struct ListItem {
/// 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>,
+ end_slot_visibility: EndSlotVisibility,
toggle: Option<bool>,
inset: bool,
on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
@@ -61,7 +67,7 @@ impl ListItem {
indent_step_size: px(12.),
start_slot: None,
end_slot: None,
- end_hover_slot: None,
+ end_slot_visibility: EndSlotVisibility::default(),
toggle: None,
inset: false,
on_click: None,
@@ -165,8 +171,14 @@ impl ListItem {
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);
+ pub fn end_slot_on_hover<E: IntoElement>(mut self, end_slot_on_hover: E) -> Self {
+ self.end_slot_visibility =
+ EndSlotVisibility::SwapOnHover(end_slot_on_hover.into_any_element());
+ self
+ }
+
+ pub fn show_end_slot_on_hover(mut self) -> Self {
+ self.end_slot_visibility = EndSlotVisibility::OnHover;
self
}
@@ -338,28 +350,31 @@ impl RenderOnce for ListItem {
.children(self.start_slot)
.children(self.children),
)
+ .when(self.end_slot.is_some(), |this| this.justify_between())
.when_some(self.end_slot, |this, end_slot| {
- this.justify_between().child(
- h_flex()
+ this.child(match self.end_slot_visibility {
+ EndSlotVisibility::Always => {
+ h_flex().flex_shrink().overflow_hidden().child(end_slot)
+ }
+ EndSlotVisibility::OnHover => h_flex()
.flex_shrink()
.overflow_hidden()
- .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_flex()
- .h_full()
- .absolute()
- .right(DynamicSpacing::Base06.rems(cx))
- .top_0()
.visible_on_hover("list_item")
- .child(end_hover_slot),
- )
+ .child(end_slot),
+ EndSlotVisibility::SwapOnHover(hover_slot) => h_flex()
+ .relative()
+ .flex_shrink()
+ .child(h_flex().visible_on_hover("list_item").child(hover_slot))
+ .child(
+ h_flex()
+ .absolute()
+ .inset_0()
+ .justify_end()
+ .overflow_hidden()
+ .group_hover("list_item", |this| this.invisible())
+ .child(end_slot),
+ ),
+ })
}),
)
}