Add tab switcher (#7987)

The Tab Switcher implementation (#7653):
- `ctrl-tab` opens the Tab Switcher and moves selection to the
previously selcted tab. It also cycles selection forward.
- `ctrl-shift-tab` opens the Tab Switcher and moves selection to the
last tab in the list. It also cycles selection backward.
- Tab is selected and the Tab Switcher is closed on the shortcut
modifier key (`ctrl` by default) release.
- List items are in reverse activation history order.
- The list reacts to the item changes in background (new tab, tab
closed, tab title changed etc.)

Intentionally not in scope of this PR:
- File icons
- Close buttons

I will come back to these features. I think they need to be implemented
in separate PRs, and be synchronized with changes in how tabs are
rendered, to reuse the code as it's done in the current implementation.
The Tab Switcher looks usable even without them.

Known Issues:

Tab Switcher doesn't react to mouse click on a list item. It's not a tab
switcher specific problem, it looks like ctrl-clicks are not handled the
same way in Zed as cmd-clicks. For instance, menu items can be activated
with cmd-click, but don't react to ctrl-click. Since the Tab Switcher's
default keybinding is `ctrl-tab`, the user can only click an item with
`ctrl` pushed down, thus preventing `on_click()` from firing.

fixes #7653, #7321

Release Notes:

- Added Tab Switcher which is accessible via `ctrl-tab` and
`ctrl-shift-tab` (#7653) (#7321)

Related issues:

- Unblocks #7356, I hope 😄

How it looks and works (it's only `ctrl-tab`'s and `ctrl-shift-tab`'s,
no `enter`'s or mouse clicks):


https://github.com/zed-industries/zed/assets/2101250/4ad4ec6a-5314-481b-8b35-7ac85e43eb92

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
Co-authored-by: Mikayla Maki <mikayla@zed.dev>
This commit is contained in:
Andrew Lygin 2024-03-27 21:15:08 +03:00 committed by GitHub
parent 9c22009e7b
commit 894b39a918
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 715 additions and 56 deletions

View file

@ -422,6 +422,10 @@ impl Pane {
self.active_item_index
}
pub fn activation_history(&self) -> &Vec<EntityId> {
&self.activation_history
}
pub fn set_can_split(&mut self, can_split: bool, cx: &mut ViewContext<Self>) {
self.can_split = can_split;
cx.notify();
@ -1309,17 +1313,7 @@ impl Pane {
let label = item.tab_content(Some(detail), is_active, cx);
let close_side = &ItemSettings::get_global(cx).close_position;
let indicator = maybe!({
let indicator_color = match (item.has_conflict(cx), item.is_dirty(cx)) {
(true, _) => Color::Warning,
(_, true) => Color::Accent,
(false, false) => return None,
};
Some(Indicator::dot().color(indicator_color))
});
let indicator = render_item_indicator(item.boxed_clone(), cx);
let item_id = item.item_id();
let is_first_item = ix == 0;
let is_last_item = ix == self.items.len() - 1;
@ -1529,7 +1523,7 @@ impl Pane {
self.items
.iter()
.enumerate()
.zip(self.tab_details(cx))
.zip(tab_details(&self.items, cx))
.map(|((ix, item), detail)| self.render_tab(ix, item, detail, cx)),
)
.child(
@ -1576,43 +1570,6 @@ impl Pane {
.child(overlay().anchor(AnchorCorner::TopRight).child(menu.clone()))
}
fn tab_details(&self, cx: &AppContext) -> Vec<usize> {
let mut tab_details = self.items.iter().map(|_| 0).collect::<Vec<_>>();
let mut tab_descriptions = HashMap::default();
let mut done = false;
while !done {
done = true;
// Store item indices by their tab description.
for (ix, (item, detail)) in self.items.iter().zip(&tab_details).enumerate() {
if let Some(description) = item.tab_description(*detail, cx) {
if *detail == 0
|| Some(&description) != item.tab_description(detail - 1, cx).as_ref()
{
tab_descriptions
.entry(description)
.or_insert(Vec::new())
.push(ix);
}
}
}
// If two or more items have the same tab description, increase eir level
// of detail and try again.
for (_, item_ixs) in tab_descriptions.drain() {
if item_ixs.len() > 1 {
done = false;
for ix in item_ixs {
tab_details[ix] += 1;
}
}
}
}
tab_details
}
pub fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
self.zoomed = zoomed;
cx.notify();
@ -2127,6 +2084,54 @@ fn dirty_message_for(buffer_path: Option<ProjectPath>) -> String {
format!("{path} contains unsaved edits. Do you want to save it?")
}
pub fn tab_details(items: &Vec<Box<dyn ItemHandle>>, cx: &AppContext) -> Vec<usize> {
let mut tab_details = items.iter().map(|_| 0).collect::<Vec<_>>();
let mut tab_descriptions = HashMap::default();
let mut done = false;
while !done {
done = true;
// Store item indices by their tab description.
for (ix, (item, detail)) in items.iter().zip(&tab_details).enumerate() {
if let Some(description) = item.tab_description(*detail, cx) {
if *detail == 0
|| Some(&description) != item.tab_description(detail - 1, cx).as_ref()
{
tab_descriptions
.entry(description)
.or_insert(Vec::new())
.push(ix);
}
}
}
// If two or more items have the same tab description, increase their level
// of detail and try again.
for (_, item_ixs) in tab_descriptions.drain() {
if item_ixs.len() > 1 {
done = false;
for ix in item_ixs {
tab_details[ix] += 1;
}
}
}
}
tab_details
}
pub fn render_item_indicator(item: Box<dyn ItemHandle>, cx: &WindowContext) -> Option<Indicator> {
maybe!({
let indicator_color = match (item.has_conflict(cx), item.is_dirty(cx)) {
(true, _) => Color::Warning,
(_, true) => Color::Accent,
(false, false) => return None,
};
Some(Indicator::dot().color(indicator_color))
})
}
#[cfg(test)]
mod tests {
use super::*;