use std::{collections::HashSet, sync::Arc};
use gpui::{Context, ListOffset, Pixels, SharedString, Window};
use super::{SidebarNav, SidebarNavEvent, SidebarNavItem, SidebarSection};
fn collect_item_ids(items: &[SidebarNavItem], ids: &mut HashSet<SharedString>) -> bool {
items
.iter()
.all(|item| ids.insert(item.id.clone()) && collect_item_ids(&item.children, ids))
}
pub(super) fn snapshot_ids_are_unique(sections: &[SidebarSection]) -> bool {
let mut section_ids = HashSet::new();
let mut item_ids = HashSet::new();
sections.iter().all(|section| {
section_ids.insert(section.id.clone()) && collect_item_ids(§ion.items, &mut item_ids)
})
}
pub(super) fn collect_parent_ids(items: &[SidebarNavItem], ids: &mut HashSet<SharedString>) {
for item in items {
if !item.children.is_empty() {
ids.insert(item.id.clone());
collect_parent_ids(&item.children, ids);
}
}
}
fn item_matches(item: &SidebarNavItem, query: &str) -> bool {
item.label.to_lowercase().contains(query)
|| item
.badge
.as_ref()
.is_some_and(|badge| badge.to_lowercase().contains(query))
}
#[derive(Debug, Default)]
struct FilterProjection {
sections: HashSet<SharedString>,
items: HashSet<SharedString>,
}
fn retain_item(
item: &SidebarNavItem,
query: &str,
inherited: bool,
retained: &mut HashSet<SharedString>,
) -> bool {
let matched = inherited || item_matches(item, query);
let mut keep = matched;
for child in item.children.iter() {
keep |= retain_item(child, query, matched, retained);
}
if keep {
retained.insert(item.id.clone());
}
keep
}
fn filter_projection(sections: &[SidebarSection], query: &str) -> FilterProjection {
let mut projection = FilterProjection::default();
for section in sections {
let matched = section.label.to_lowercase().contains(query);
let mut keep = matched;
for item in section.items.iter() {
keep |= retain_item(item, query, matched, &mut projection.items);
}
if keep {
projection.sections.insert(section.id.clone());
}
}
projection
}
fn collect_ancestors(
items: &[SidebarNavItem],
target: &SharedString,
ancestors: &mut HashSet<SharedString>,
) -> bool {
for item in items {
if item.id == *target {
return true;
}
if collect_ancestors(&item.children, target, ancestors) {
ancestors.insert(item.id.clone());
return true;
}
}
false
}
fn find_item<'a>(item: &'a SidebarNavItem, id: &SharedString) -> Option<&'a SidebarNavItem> {
(item.id == *id)
.then_some(item)
.or_else(|| item.children.iter().find_map(|child| find_item(child, id)))
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(super) struct VisibleRow {
pub(super) id: SharedString,
pub(super) label: SharedString,
pub(super) icon: Option<SharedString>,
pub(super) badge: Option<SharedString>,
pub(super) parent: Option<SharedString>,
pub(super) level: usize,
pub(super) indent: usize,
pub(super) position: usize,
pub(super) set_size: usize,
pub(super) header: bool,
pub(super) disabled: bool,
pub(super) has_children: bool,
pub(super) expanded: bool,
pub(super) active: bool,
pub(super) contains_active: bool,
}
struct RowContext<'a> {
projection: Option<&'a FilterProjection>,
expanded: &'a HashSet<SharedString>,
active: Option<&'a SharedString>,
ancestors: &'a HashSet<SharedString>,
filtering: bool,
collapsed: bool,
}
fn visible_children<'a>(
items: &'a [SidebarNavItem],
projection: Option<&'a FilterProjection>,
) -> impl Iterator<Item = &'a SidebarNavItem> {
items
.iter()
.filter(move |item| projection.is_none_or(|projection| projection.items.contains(&item.id)))
}
pub(super) fn visible_rows(
sections: &[SidebarSection],
query: &str,
expanded: &HashSet<SharedString>,
active: Option<&SharedString>,
collapsed: bool,
) -> Arc<[VisibleRow]> {
let query = query.trim().to_lowercase();
let filtering = !query.is_empty();
let projection = filtering.then(|| filter_projection(sections, &query));
let projection = projection.as_ref();
let active =
active.filter(|id| projection.is_none_or(|projection| projection.items.contains(*id)));
let mut ancestors = HashSet::new();
if let Some(active) = active {
for section in sections {
if collect_ancestors(§ion.items, active, &mut ancestors) {
break;
}
}
}
let context = RowContext {
projection,
expanded,
active,
ancestors: &ancestors,
filtering,
collapsed,
};
let visible: Vec<&SidebarSection> = sections
.iter()
.filter(|section| {
projection.is_none_or(|projection| projection.sections.contains(§ion.id))
})
.collect();
let section_count = visible.len();
let root_level = if collapsed { 1 } else { 2 };
let mut rows = Vec::new();
for (index, section) in visible.into_iter().enumerate() {
if !collapsed {
let children = visible_children(§ion.items, context.projection)
.next()
.is_some();
rows.push(VisibleRow {
id: section.id.clone(),
label: section.label.clone(),
icon: None,
badge: None,
parent: None,
level: 1,
indent: 0,
position: index + 1,
set_size: section_count,
header: true,
disabled: false,
has_children: children,
expanded: children,
active: false,
contains_active: false,
});
}
push_rows(
§ion.items,
§ion.id,
root_level,
0,
&context,
&mut rows,
);
}
Arc::from(rows)
}
fn push_rows(
items: &[SidebarNavItem],
parent: &SharedString,
level: usize,
indent: usize,
context: &RowContext,
rows: &mut Vec<VisibleRow>,
) {
let set_size = visible_children(items, context.projection).count();
for (index, item) in visible_children(items, context.projection).enumerate() {
let contains_active = context.ancestors.contains(&item.id);
let has_children = visible_children(&item.children, context.projection)
.next()
.is_some();
let expanded = has_children
&& (context.filtering || context.expanded.contains(&item.id) || contains_active);
let active = context.active == Some(&item.id) || (context.collapsed && contains_active);
rows.push(VisibleRow {
id: item.id.clone(),
label: item.label.clone(),
icon: item.icon.clone(),
badge: item.badge.clone(),
parent: Some(parent.clone()),
level,
indent,
position: index + 1,
set_size,
header: false,
disabled: item.disabled,
has_children,
expanded,
active,
contains_active,
});
if expanded && !context.collapsed {
push_rows(
&item.children,
&item.id,
level + 1,
indent + 1,
context,
rows,
);
}
}
}
impl SidebarNav {
pub(super) fn rebuild_rows(&mut self, cx: &mut Context<Self>) {
let anchor = self.scroll_anchor();
self.rows = visible_rows(
&self.sections,
&self.query,
&self.expanded,
self.active_item.as_ref(),
self.collapsed,
);
self.row_list.reset(self.rows.len());
self.restore_anchor(anchor);
cx.notify();
}
fn scroll_anchor(&self) -> Option<(SharedString, Pixels)> {
let offset = self.row_list.logical_scroll_top();
self.rows
.get(offset.item_ix)
.map(|row| (row.id.clone(), offset.offset_in_item))
}
fn restore_anchor(&self, anchor: Option<(SharedString, Pixels)>) {
let Some((id, offset_in_item)) = anchor else {
return;
};
let Some(item_ix) = self.rows.iter().position(|row| row.id == id) else {
return;
};
self.row_list.scroll_to(ListOffset {
item_ix,
offset_in_item,
});
}
pub(super) fn resolve_layout(&mut self, rem_size: Pixels, cx: &mut Context<Self>) {
if !self.resolved_layout.observe(rem_size) {
return;
}
let anchor = self.scroll_anchor();
self.row_list.remeasure();
self.restore_anchor(anchor);
cx.notify();
}
fn focused_row_index(&self) -> Option<usize> {
let focused = self.focused_row.as_ref()?;
self.rows.iter().position(|row| row.id == *focused)
}
fn roving_row_index(&self) -> Option<usize> {
if self.rows.is_empty() {
return None;
}
self.focused_row_index().or_else(|| {
let first = self.row_list.logical_scroll_top().item_ix;
(first < self.rows.len()).then_some(first)
})
}
pub(super) fn roving_row_id(&self) -> Option<SharedString> {
self.roving_row_index()
.and_then(|index| self.rows.get(index))
.map(|row| row.id.clone())
}
fn focus_row(&mut self, index: usize, window: &mut Window, cx: &mut Context<Self>) -> bool {
let Some(row) = self.rows.get(index) else {
return false;
};
self.focused_row = Some(row.id.clone());
self.row_list.scroll_to_reveal_item(index);
self.tree_focus.focus(window, cx);
cx.notify();
true
}
fn step(&self, from: usize, forward: bool) -> Option<usize> {
let mut index = from;
loop {
index = if forward {
index.checked_add(1)?
} else {
index.checked_sub(1)?
};
if !self.rows.get(index)?.disabled {
return Some(index);
}
}
}
fn bound(&self, last: bool) -> Option<usize> {
let mut candidates = (0..self.rows.len())
.filter(|index| self.rows.get(*index).is_some_and(|row| !row.disabled));
if last {
candidates.next_back()
} else {
candidates.next()
}
}
fn first_child(&self, index: usize) -> Option<usize> {
let row = self.rows.get(index)?;
self.rows
.iter()
.enumerate()
.skip(index + 1)
.take_while(|(_, candidate)| candidate.level > row.level)
.find(|(_, candidate)| {
candidate.parent.as_ref() == Some(&row.id) && !candidate.disabled
})
.map(|(child, _)| child)
}
fn parent_row(&self, index: usize) -> Option<usize> {
let parent = self.rows.get(index)?.parent.as_ref()?;
self.rows
.iter()
.position(|row| row.id == *parent && !row.disabled)
}
fn set_expanded(&mut self, id: SharedString, expanded: bool, cx: &mut Context<Self>) {
let changed = if expanded {
self.expanded.insert(id)
} else {
self.expanded.remove(&id)
};
if changed {
self.rebuild_rows(cx);
}
}
pub(super) fn navigate(
&mut self,
key: &str,
window: &mut Window,
cx: &mut Context<Self>,
) -> bool {
let Some(current) = self.roving_row_index() else {
return false;
};
let Some(row) = self.rows.get(current).cloned() else {
return false;
};
match key {
"up" => self
.step(current, false)
.is_some_and(|index| self.focus_row(index, window, cx)),
"down" => self
.step(current, true)
.is_some_and(|index| self.focus_row(index, window, cx)),
"home" => self
.bound(false)
.is_some_and(|index| self.focus_row(index, window, cx)),
"end" => self
.bound(true)
.is_some_and(|index| self.focus_row(index, window, cx)),
"right" => {
if row.has_children && !row.expanded && !row.header {
self.set_expanded(row.id.clone(), true, cx);
true
} else {
self.first_child(current)
.is_some_and(|index| self.focus_row(index, window, cx))
}
}
"left" => {
if !row.header && row.expanded && self.expanded.contains(&row.id) {
self.set_expanded(row.id.clone(), false, cx);
true
} else {
self.parent_row(current)
.is_some_and(|index| self.focus_row(index, window, cx))
}
}
"enter" | "space" => {
if row.header {
return false;
}
self.activate_item(row.id.clone(), window, cx);
true
}
_ => false,
}
}
pub(super) fn activate_item(
&mut self,
item_id: SharedString,
window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(item) = self
.sections
.iter()
.flat_map(|section| section.items.iter())
.find_map(|item| find_item(item, &item_id))
else {
return;
};
if item.disabled {
return;
}
let has_children = !item.children.is_empty();
self.focused_row = Some(item_id.clone());
self.tree_focus.focus(window, cx);
if has_children && !self.expanded.remove(&item_id) {
self.expanded.insert(item_id.clone());
}
self.rebuild_rows(cx);
cx.emit(SidebarNavEvent::Selected {
id: self.id.clone(),
item_id,
});
}
}