Feat: collapsible help
This commit is contained in:
@@ -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)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user