Feat: collapsible help

This commit is contained in:
2026-02-16 23:43:25 +01:00
parent 540f59dcf5
commit 524e686b3a
12 changed files with 597 additions and 119 deletions

View File

@@ -8,11 +8,17 @@ use crate::theme;
pub struct CategoryItem<'a> {
pub label: &'a str,
pub is_section: bool,
pub collapsed: bool,
}
pub enum Selection {
Item(usize),
Section(usize),
}
pub struct CategoryList<'a> {
items: &'a [CategoryItem<'a>],
selected: usize,
selection: Selection,
focused: bool,
title: &'a str,
section_color: Color,
@@ -23,11 +29,11 @@ pub struct CategoryList<'a> {
}
impl<'a> CategoryList<'a> {
pub fn new(items: &'a [CategoryItem<'a>], selected: usize) -> Self {
pub fn new(items: &'a [CategoryItem<'a>], selection: Selection) -> Self {
let theme = theme::get();
Self {
items,
selected,
selection,
focused: false,
title: "",
section_color: theme.ui.text_dim,
@@ -63,25 +69,44 @@ impl<'a> CategoryList<'a> {
self
}
/// Build the visible items list, filtering out children of collapsed sections.
/// Returns (item, section_index_if_section, item_index_if_item).
fn visible_items(&self) -> Vec<(&CategoryItem<'a>, Option<usize>, Option<usize>)> {
let mut result = Vec::new();
let mut skipping = false;
let mut section_idx = 0usize;
let mut item_idx = 0usize;
for item in self.items.iter() {
if item.is_section {
skipping = item.collapsed;
result.push((item, Some(section_idx), None));
section_idx += 1;
} else if !skipping {
result.push((item, None, Some(item_idx)));
item_idx += 1;
} else {
item_idx += 1;
}
}
result
}
pub fn render(self, frame: &mut Frame, area: Rect) {
let theme = theme::get();
let visible = self.visible_items();
let visible_height = area.height.saturating_sub(2) as usize;
let total_items = self.items.len();
let total_items = visible.len();
let selected_visual_idx = {
let mut visual = 0;
let mut selectable_count = 0;
for item in self.items.iter() {
if !item.is_section {
if selectable_count == self.selected {
break;
}
selectable_count += 1;
}
visual += 1;
}
visual
let selected_visual_idx = match &self.selection {
Selection::Item(sel) => visible
.iter()
.position(|(_, _, item_idx)| *item_idx == Some(*sel))
.unwrap_or(0),
Selection::Section(sel) => visible
.iter()
.position(|(_, sec_idx, _)| *sec_idx == Some(*sel))
.unwrap_or(0),
};
let scroll = if selected_visual_idx < visible_height / 2 {
@@ -92,24 +117,35 @@ impl<'a> CategoryList<'a> {
selected_visual_idx.saturating_sub(visible_height / 2)
};
let mut selectable_idx = self.items
.iter()
.take(scroll)
.filter(|e| !e.is_section)
.count();
let is_dimmed = self.dimmed_color.is_some();
let items: Vec<ListItem> = self.items
let items: Vec<ListItem> = visible
.iter()
.skip(scroll)
.take(visible_height)
.map(|item| {
.enumerate()
.map(|(vis_offset, (item, sec_idx, _itm_idx))| {
let visual_pos = scroll + vis_offset;
if item.is_section {
let style = Style::new().fg(self.section_color);
ListItem::new(format!("{}", item.label)).style(style)
let is_selected =
matches!(&self.selection, Selection::Section(s) if Some(*s) == *sec_idx);
let arrow = if item.collapsed { "" } else { "" };
let style = if is_selected && self.focused {
Style::new()
.fg(self.focused_color)
.add_modifier(Modifier::BOLD)
} else if is_selected {
Style::new()
.fg(self.selected_color)
.add_modifier(Modifier::BOLD)
} else {
Style::new().fg(self.section_color)
};
let prefix = if is_selected && !is_dimmed { "> " } else { "" };
ListItem::new(format!("{prefix}{arrow} {}", item.label)).style(style)
} else {
let is_selected = selectable_idx == self.selected;
let is_selected = visual_pos == selected_visual_idx
&& matches!(&self.selection, Selection::Item(_));
let style = if let Some(dim_color) = self.dimmed_color {
Style::new().fg(dim_color)
} else if is_selected && self.focused {
@@ -123,8 +159,11 @@ impl<'a> CategoryList<'a> {
} else {
Style::new().fg(self.normal_color)
};
let prefix = if is_selected && !is_dimmed { "> " } else { " " };
selectable_idx += 1;
let prefix = if is_selected && !is_dimmed {
"> "
} else {
" "
};
ListItem::new(format!("{prefix}{}", item.label)).style(style)
}
})

View File

@@ -21,7 +21,7 @@ mod vu_meter;
mod waveform;
pub use active_patterns::{ActivePatterns, MuteStatus};
pub use category_list::{CategoryItem, CategoryList};
pub use category_list::{CategoryItem, CategoryList, Selection};
pub use confirm::ConfirmModal;
pub use editor::{fuzzy_match, CompletionCandidate, Editor};
pub use file_browser::FileBrowserModal;