diff --git a/assets/icons/maximize_alt.svg b/assets/icons/maximize_alt.svg new file mode 100644 index 0000000000000000000000000000000000000000..b8b8705f902c2469ed959f93f89ca3caf3b8fc51 --- /dev/null +++ b/assets/icons/maximize_alt.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/crates/agent_ui/src/config_options.rs b/crates/agent_ui/src/config_options.rs index b8cf7e5d57921c7710392911829fc2b5045a0f90..44c0baa232222c0ba7c1d54acdecaabacfa85f12 100644 --- a/crates/agent_ui/src/config_options.rs +++ b/crates/agent_ui/src/config_options.rs @@ -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 { diff --git a/crates/agent_ui/src/ui/model_selector_components.rs b/crates/agent_ui/src/ui/model_selector_components.rs index 01ba6c4511854e83b97b1fc053e41e5d0e82ff1e..88bf546a0e7beef53c8043fd04f8e6e9e5e92c88 100644 --- a/crates/agent_ui/src/ui/model_selector_components.rs +++ b/crates/agent_ui/src/ui/model_selector_components.rs @@ -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") diff --git a/crates/git_ui/src/branch_picker.rs b/crates/git_ui/src/branch_picker.rs index 438df6839949d46d3ba8e0509995beb1300b7c80..83c8119a077ac1c024dbb3b3df948f762b072ec1 100644 --- a/crates/git_ui/src/branch_picker.rs +++ b/crates/git_ui/src/branch_picker.rs @@ -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() }, ), ) diff --git a/crates/git_ui/src/stash_picker.rs b/crates/git_ui/src/stash_picker.rs index 9987190f45b73f3f1132ce1295de6f412022abe2..2d3515e833e4d353c323f533f1f0f39bb1d76561 100644 --- a/crates/git_ui/src/stash_picker.rs +++ b/crates/git_ui/src/stash_picker.rs @@ -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>) -> Option { + if self.matches.is_empty() { + return None; + } + let focus_handle = self.focus_handle.clone(); Some( diff --git a/crates/git_ui/src/worktree_picker.rs b/crates/git_ui/src/worktree_picker.rs index 3e14b56f9bf4a95452855bc6cbef6f764e2b3530..c3e2259e411c7a3a56a36b92735f8d5e014e53d7 100644 --- a/crates/git_ui/src/worktree_picker.rs +++ b/crates/git_ui/src/worktree_picker.rs @@ -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() }), ) } diff --git a/crates/icons/src/icons.rs b/crates/icons/src/icons.rs index ad7faa6664d1ddc618c8984781a244af7dda6c97..6929ae4e4ca8ca0ee00c9793c948892043dd6dd6 100644 --- a/crates/icons/src/icons.rs +++ b/crates/icons/src/icons.rs @@ -174,6 +174,7 @@ pub enum IconName { LockOutlined, MagnifyingGlass, Maximize, + MaximizeAlt, Menu, MenuAltTemp, Mic, diff --git a/crates/recent_projects/src/recent_projects.rs b/crates/recent_projects/src/recent_projects.rs index 4dc06036ef8416fd859cc815ab090ba5896c0040..22987f6c56669e1972a9bfc940449991d9f55642 100644 --- a/crates/recent_projects/src/recent_projects.rs +++ b/crates/recent_projects/src/recent_projects.rs @@ -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(), ) } diff --git a/crates/recent_projects/src/remote_servers.rs b/crates/recent_projects/src/remote_servers.rs index f7054687579155d4895ae191de1b7fa7cd14fbf6..26592a8035d50caa4e267a5478d5aceb9fba6e3e 100644 --- a/crates/recent_projects/src/remote_servers.rs +++ b/crates/recent_projects/src/remote_servers.rs @@ -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::(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| { diff --git a/crates/rules_library/src/rules_library.rs b/crates/rules_library/src/rules_library.rs index 23e7b83772f755089c49f824719af389ec589bd9..7e5a56f22d48c4d51f60d7d200dc8384582beb23 100644 --- a/crates/rules_library/src/rules_library.rs +++ b/crates/rules_library/src/rules_library.rs @@ -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) diff --git a/crates/tab_switcher/src/tab_switcher.rs b/crates/tab_switcher/src/tab_switcher.rs index 0fb13c85d21797e4d57728c88fc8bb014a898f78..d1e19ea4faee8d8259d06e2c24875faac7a0117c 100644 --- a/crates/tab_switcher/src/tab_switcher.rs +++ b/crates/tab_switcher/src/tab_switcher.rs @@ -875,7 +875,7 @@ impl PickerDelegate for TabSwitcherDelegate { el.end_slot::(close_button) } else { el.end_slot::(indicator) - .end_hover_slot::(close_button) + .end_slot_on_hover::(close_button) } }), ) diff --git a/crates/tasks_ui/src/modal.rs b/crates/tasks_ui/src/modal.rs index 34f0cd809692d649bcfbabb7952f3075618ead04..285a07c9562849b26b4cbba3de3979614384d875 100644 --- a/crates/tasks_ui/src/modal.rs +++ b/crates/tasks_ui/src/modal.rs @@ -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 } diff --git a/crates/ui/src/components/list/list_item.rs b/crates/ui/src/components/list/list_item.rs index 0d3efc024f1a202947fd0e7b0dab917c40ae8337..9a764efd58cfd3365d92e534a715a0f23ce46e90 100644 --- a/crates/ui/src/components/list/list_item.rs +++ b/crates/ui/src/components/list/list_item.rs @@ -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, - /// A slot for content that appears on hover after the children - /// It will obscure the `end_slot` when visible. - end_hover_slot: Option, + end_slot_visibility: EndSlotVisibility, toggle: Option, inset: bool, on_click: Option>, @@ -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(mut self, end_hover_slot: impl Into>) -> Self { - self.end_hover_slot = end_hover_slot.into().map(IntoElement::into_any_element); + pub fn end_slot_on_hover(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), + ), + }) }), ) }