Feat: collapsible help
This commit is contained in:
@@ -8,11 +8,17 @@ use crate::theme;
|
|||||||
pub struct CategoryItem<'a> {
|
pub struct CategoryItem<'a> {
|
||||||
pub label: &'a str,
|
pub label: &'a str,
|
||||||
pub is_section: bool,
|
pub is_section: bool,
|
||||||
|
pub collapsed: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Selection {
|
||||||
|
Item(usize),
|
||||||
|
Section(usize),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct CategoryList<'a> {
|
pub struct CategoryList<'a> {
|
||||||
items: &'a [CategoryItem<'a>],
|
items: &'a [CategoryItem<'a>],
|
||||||
selected: usize,
|
selection: Selection,
|
||||||
focused: bool,
|
focused: bool,
|
||||||
title: &'a str,
|
title: &'a str,
|
||||||
section_color: Color,
|
section_color: Color,
|
||||||
@@ -23,11 +29,11 @@ pub struct CategoryList<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> 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();
|
let theme = theme::get();
|
||||||
Self {
|
Self {
|
||||||
items,
|
items,
|
||||||
selected,
|
selection,
|
||||||
focused: false,
|
focused: false,
|
||||||
title: "",
|
title: "",
|
||||||
section_color: theme.ui.text_dim,
|
section_color: theme.ui.text_dim,
|
||||||
@@ -63,25 +69,44 @@ impl<'a> CategoryList<'a> {
|
|||||||
self
|
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) {
|
pub fn render(self, frame: &mut Frame, area: Rect) {
|
||||||
let theme = theme::get();
|
let theme = theme::get();
|
||||||
|
let visible = self.visible_items();
|
||||||
|
|
||||||
let visible_height = area.height.saturating_sub(2) as usize;
|
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 selected_visual_idx = match &self.selection {
|
||||||
let mut visual = 0;
|
Selection::Item(sel) => visible
|
||||||
let mut selectable_count = 0;
|
.iter()
|
||||||
for item in self.items.iter() {
|
.position(|(_, _, item_idx)| *item_idx == Some(*sel))
|
||||||
if !item.is_section {
|
.unwrap_or(0),
|
||||||
if selectable_count == self.selected {
|
Selection::Section(sel) => visible
|
||||||
break;
|
.iter()
|
||||||
}
|
.position(|(_, sec_idx, _)| *sec_idx == Some(*sel))
|
||||||
selectable_count += 1;
|
.unwrap_or(0),
|
||||||
}
|
|
||||||
visual += 1;
|
|
||||||
}
|
|
||||||
visual
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let scroll = if selected_visual_idx < visible_height / 2 {
|
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)
|
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 is_dimmed = self.dimmed_color.is_some();
|
||||||
|
|
||||||
let items: Vec<ListItem> = self.items
|
let items: Vec<ListItem> = visible
|
||||||
.iter()
|
.iter()
|
||||||
.skip(scroll)
|
.skip(scroll)
|
||||||
.take(visible_height)
|
.take(visible_height)
|
||||||
.map(|item| {
|
.enumerate()
|
||||||
|
.map(|(vis_offset, (item, sec_idx, _itm_idx))| {
|
||||||
|
let visual_pos = scroll + vis_offset;
|
||||||
if item.is_section {
|
if item.is_section {
|
||||||
let style = Style::new().fg(self.section_color);
|
let is_selected =
|
||||||
ListItem::new(format!("─ {} ─", item.label)).style(style)
|
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 {
|
} else {
|
||||||
let is_selected = selectable_idx == self.selected;
|
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 = visual_pos == selected_visual_idx
|
||||||
|
&& matches!(&self.selection, Selection::Item(_));
|
||||||
let style = if let Some(dim_color) = self.dimmed_color {
|
let style = if let Some(dim_color) = self.dimmed_color {
|
||||||
Style::new().fg(dim_color)
|
Style::new().fg(dim_color)
|
||||||
} else if is_selected && self.focused {
|
} else if is_selected && self.focused {
|
||||||
@@ -123,8 +159,11 @@ impl<'a> CategoryList<'a> {
|
|||||||
} else {
|
} else {
|
||||||
Style::new().fg(self.normal_color)
|
Style::new().fg(self.normal_color)
|
||||||
};
|
};
|
||||||
let prefix = if is_selected && !is_dimmed { "> " } else { " " };
|
let prefix = if is_selected && !is_dimmed {
|
||||||
selectable_idx += 1;
|
"> "
|
||||||
|
} else {
|
||||||
|
" "
|
||||||
|
};
|
||||||
ListItem::new(format!("{prefix}{}", item.label)).style(style)
|
ListItem::new(format!("{prefix}{}", item.label)).style(style)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ mod vu_meter;
|
|||||||
mod waveform;
|
mod waveform;
|
||||||
|
|
||||||
pub use active_patterns::{ActivePatterns, MuteStatus};
|
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 confirm::ConfirmModal;
|
||||||
pub use editor::{fuzzy_match, CompletionCandidate, Editor};
|
pub use editor::{fuzzy_match, CompletionCandidate, Editor};
|
||||||
pub use file_browser::FileBrowserModal;
|
pub use file_browser::FileBrowserModal;
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ use std::sync::atomic::Ordering;
|
|||||||
|
|
||||||
use super::{InputContext, InputResult};
|
use super::{InputContext, InputResult};
|
||||||
use crate::commands::AppCommand;
|
use crate::commands::AppCommand;
|
||||||
|
use crate::model::categories;
|
||||||
|
use crate::model::docs;
|
||||||
use crate::state::{ConfirmAction, DictFocus, FlashKind, HelpFocus, Modal};
|
use crate::state::{ConfirmAction, DictFocus, FlashKind, HelpFocus, Modal};
|
||||||
|
|
||||||
pub(super) fn handle_help_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
pub(super) fn handle_help_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||||
@@ -30,6 +32,12 @@ pub(super) fn handle_help_page(ctx: &mut InputContext, key: KeyEvent) -> InputRe
|
|||||||
ctx.app.ui.help_focused_block = None;
|
ctx.app.ui.help_focused_block = None;
|
||||||
}
|
}
|
||||||
KeyCode::Tab => ctx.dispatch(AppCommand::HelpToggleFocus),
|
KeyCode::Tab => ctx.dispatch(AppCommand::HelpToggleFocus),
|
||||||
|
KeyCode::Left if ctx.app.ui.help_focus == HelpFocus::Topics => {
|
||||||
|
collapse_help_section(ctx);
|
||||||
|
}
|
||||||
|
KeyCode::Right if ctx.app.ui.help_focus == HelpFocus::Topics => {
|
||||||
|
expand_help_section(ctx);
|
||||||
|
}
|
||||||
KeyCode::Char('n') if ctx.app.ui.help_focus == HelpFocus::Content => {
|
KeyCode::Char('n') if ctx.app.ui.help_focus == HelpFocus::Content => {
|
||||||
navigate_code_block(ctx, true);
|
navigate_code_block(ctx, true);
|
||||||
}
|
}
|
||||||
@@ -130,6 +138,66 @@ fn execute_focused_block(ctx: &mut InputContext) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn collapse_help_section(ctx: &mut InputContext) {
|
||||||
|
if let Some(s) = ctx.app.ui.help_on_section {
|
||||||
|
if ctx.app.ui.help_collapsed.get(s).copied().unwrap_or(false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let section = match ctx.app.ui.help_on_section {
|
||||||
|
Some(s) => s,
|
||||||
|
None => docs::section_index_for_topic(ctx.app.ui.help_topic),
|
||||||
|
};
|
||||||
|
if let Some(v) = ctx.app.ui.help_collapsed.get_mut(section) {
|
||||||
|
*v = true;
|
||||||
|
}
|
||||||
|
ctx.app.ui.help_on_section = Some(section);
|
||||||
|
ctx.app.ui.help_focused_block = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expand_help_section(ctx: &mut InputContext) {
|
||||||
|
let Some(section) = ctx.app.ui.help_on_section else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if let Some(v) = ctx.app.ui.help_collapsed.get_mut(section) {
|
||||||
|
*v = false;
|
||||||
|
}
|
||||||
|
ctx.app.ui.help_on_section = None;
|
||||||
|
if let Some(first) = docs::first_topic_in_section(section) {
|
||||||
|
ctx.app.ui.help_topic = first;
|
||||||
|
}
|
||||||
|
ctx.app.ui.help_focused_block = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collapse_dict_section(ctx: &mut InputContext) {
|
||||||
|
if let Some(s) = ctx.app.ui.dict_on_section {
|
||||||
|
if ctx.app.ui.dict_collapsed.get(s).copied().unwrap_or(false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let section = match ctx.app.ui.dict_on_section {
|
||||||
|
Some(s) => s,
|
||||||
|
None => categories::section_index_for_category(ctx.app.ui.dict_category),
|
||||||
|
};
|
||||||
|
if let Some(v) = ctx.app.ui.dict_collapsed.get_mut(section) {
|
||||||
|
*v = true;
|
||||||
|
}
|
||||||
|
ctx.app.ui.dict_on_section = Some(section);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expand_dict_section(ctx: &mut InputContext) {
|
||||||
|
let Some(section) = ctx.app.ui.dict_on_section else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if let Some(v) = ctx.app.ui.dict_collapsed.get_mut(section) {
|
||||||
|
*v = false;
|
||||||
|
}
|
||||||
|
ctx.app.ui.dict_on_section = None;
|
||||||
|
if let Some(first) = categories::first_category_in_section(section) {
|
||||||
|
ctx.app.ui.dict_category = first;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) fn handle_dict_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
pub(super) fn handle_dict_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||||
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
|
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
|
||||||
|
|
||||||
@@ -152,6 +220,12 @@ pub(super) fn handle_dict_page(ctx: &mut InputContext, key: KeyEvent) -> InputRe
|
|||||||
ctx.dispatch(AppCommand::DictClearSearch);
|
ctx.dispatch(AppCommand::DictClearSearch);
|
||||||
}
|
}
|
||||||
KeyCode::Tab => ctx.dispatch(AppCommand::DictToggleFocus),
|
KeyCode::Tab => ctx.dispatch(AppCommand::DictToggleFocus),
|
||||||
|
KeyCode::Left if ctx.app.ui.dict_focus == DictFocus::Categories => {
|
||||||
|
collapse_dict_section(ctx);
|
||||||
|
}
|
||||||
|
KeyCode::Right if ctx.app.ui.dict_focus == DictFocus::Categories => {
|
||||||
|
expand_dict_section(ctx);
|
||||||
|
}
|
||||||
KeyCode::Char('j') | KeyCode::Down => match ctx.app.ui.dict_focus {
|
KeyCode::Char('j') | KeyCode::Down => match ctx.app.ui.dict_focus {
|
||||||
DictFocus::Categories => ctx.dispatch(AppCommand::DictNextCategory),
|
DictFocus::Categories => ctx.dispatch(AppCommand::DictNextCategory),
|
||||||
DictFocus::Words => ctx.dispatch(AppCommand::DictScrollDown(1)),
|
DictFocus::Words => ctx.dispatch(AppCommand::DictScrollDown(1)),
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ use ratatui::layout::{Constraint, Layout, Rect};
|
|||||||
use crate::commands::AppCommand;
|
use crate::commands::AppCommand;
|
||||||
use crate::page::Page;
|
use crate::page::Page;
|
||||||
use crate::state::{
|
use crate::state::{
|
||||||
DictFocus, DeviceKind, EngineSection, HelpFocus, MainLayout, MinimapMode, Modal,
|
DeviceKind, DictFocus, EngineSection, HelpFocus, MainLayout, MinimapMode, Modal, OptionsFocus,
|
||||||
OptionsFocus, PatternsColumn, SettingKind,
|
PatternsColumn, SettingKind,
|
||||||
};
|
};
|
||||||
use crate::views::{dict_view, engine_view, help_view, main_view, patterns_view};
|
use crate::views::{dict_view, engine_view, help_view, main_view, patterns_view};
|
||||||
|
|
||||||
@@ -57,10 +57,7 @@ fn top_level_layout(padded: Rect) -> (Rect, Rect, Rect) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn contains(area: Rect, col: u16, row: u16) -> bool {
|
fn contains(area: Rect, col: u16, row: u16) -> bool {
|
||||||
col >= area.x
|
col >= area.x && col < area.x + area.width && row >= area.y && row < area.y + area.height
|
||||||
&& col < area.x + area.width
|
|
||||||
&& row >= area.y
|
|
||||||
&& row < area.y + area.height
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_click(ctx: &mut InputContext, col: u16, row: u16, term: Rect) {
|
fn handle_click(ctx: &mut InputContext, col: u16, row: u16, term: Rect) {
|
||||||
@@ -319,19 +316,15 @@ fn handle_main_click(ctx: &mut InputContext, col: u16, row: u16, area: Rect) {
|
|||||||
let sequencer_area = match layout {
|
let sequencer_area = match layout {
|
||||||
MainLayout::Top => {
|
MainLayout::Top => {
|
||||||
let viz_height = if has_viz { 16 } else { 0 };
|
let viz_height = if has_viz { 16 } else { 0 };
|
||||||
let [_viz, seq] = Layout::vertical([
|
let [_viz, seq] =
|
||||||
Constraint::Length(viz_height),
|
Layout::vertical([Constraint::Length(viz_height), Constraint::Fill(1)])
|
||||||
Constraint::Fill(1),
|
|
||||||
])
|
|
||||||
.areas(main_area);
|
.areas(main_area);
|
||||||
seq
|
seq
|
||||||
}
|
}
|
||||||
MainLayout::Bottom => {
|
MainLayout::Bottom => {
|
||||||
let viz_height = if has_viz { 16 } else { 0 };
|
let viz_height = if has_viz { 16 } else { 0 };
|
||||||
let [seq, _viz] = Layout::vertical([
|
let [seq, _viz] =
|
||||||
Constraint::Fill(1),
|
Layout::vertical([Constraint::Fill(1), Constraint::Length(viz_height)])
|
||||||
Constraint::Length(viz_height),
|
|
||||||
])
|
|
||||||
.areas(main_area);
|
.areas(main_area);
|
||||||
seq
|
seq
|
||||||
}
|
}
|
||||||
@@ -497,10 +490,39 @@ fn handle_help_click(ctx: &mut InputContext, col: u16, row: u16, area: Rect) {
|
|||||||
let [topics_area, content_area] = help_view::layout(area);
|
let [topics_area, content_area] = help_view::layout(area);
|
||||||
|
|
||||||
if contains(topics_area, col, row) {
|
if contains(topics_area, col, row) {
|
||||||
use crate::model::docs::{DocEntry, DOCS};
|
use crate::model::docs::{self, DocEntry, DOCS};
|
||||||
let is_section: Vec<bool> = DOCS.iter().map(|e| matches!(e, DocEntry::Section(_))).collect();
|
let items = build_visible_list(
|
||||||
if let Some(i) = hit_test_category_list(&is_section, ctx.app.ui.help_topic, topics_area, row) {
|
&DOCS
|
||||||
ctx.dispatch(AppCommand::HelpSelectTopic(i));
|
.iter()
|
||||||
|
.map(|e| matches!(e, DocEntry::Section(_)))
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
&ctx.app.ui.help_collapsed,
|
||||||
|
);
|
||||||
|
let sel = match ctx.app.ui.help_on_section {
|
||||||
|
Some(s) => VisibleEntry::Section(s),
|
||||||
|
None => VisibleEntry::Item(ctx.app.ui.help_topic),
|
||||||
|
};
|
||||||
|
if let Some(entry) = hit_test_visible_list(&items, &sel, topics_area, row) {
|
||||||
|
match entry {
|
||||||
|
VisibleEntry::Item(i) => ctx.dispatch(AppCommand::HelpSelectTopic(i)),
|
||||||
|
VisibleEntry::Section(s) => {
|
||||||
|
let collapsed = ctx.app.ui.help_collapsed.get(s).copied().unwrap_or(false);
|
||||||
|
if collapsed {
|
||||||
|
if let Some(v) = ctx.app.ui.help_collapsed.get_mut(s) {
|
||||||
|
*v = false;
|
||||||
|
}
|
||||||
|
ctx.app.ui.help_on_section = None;
|
||||||
|
if let Some(first) = docs::first_topic_in_section(s) {
|
||||||
|
ctx.app.ui.help_topic = first;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if let Some(v) = ctx.app.ui.help_collapsed.get_mut(s) {
|
||||||
|
*v = true;
|
||||||
|
}
|
||||||
|
ctx.app.ui.help_on_section = Some(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
ctx.app.ui.help_focus = HelpFocus::Topics;
|
ctx.app.ui.help_focus = HelpFocus::Topics;
|
||||||
} else if contains(content_area, col, row) {
|
} else if contains(content_area, col, row) {
|
||||||
@@ -514,10 +536,39 @@ fn handle_dict_click(ctx: &mut InputContext, col: u16, row: u16, area: Rect) {
|
|||||||
let (_header_area, [cat_area, words_area]) = dict_view::layout(area);
|
let (_header_area, [cat_area, words_area]) = dict_view::layout(area);
|
||||||
|
|
||||||
if contains(cat_area, col, row) {
|
if contains(cat_area, col, row) {
|
||||||
use crate::model::categories::{CatEntry, CATEGORIES};
|
use crate::model::categories::{self, CatEntry, CATEGORIES};
|
||||||
let is_section: Vec<bool> = CATEGORIES.iter().map(|e| matches!(e, CatEntry::Section(_))).collect();
|
let items = build_visible_list(
|
||||||
if let Some(i) = hit_test_category_list(&is_section, ctx.app.ui.dict_category, cat_area, row) {
|
&CATEGORIES
|
||||||
ctx.dispatch(AppCommand::DictSelectCategory(i));
|
.iter()
|
||||||
|
.map(|e| matches!(e, CatEntry::Section(_)))
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
&ctx.app.ui.dict_collapsed,
|
||||||
|
);
|
||||||
|
let sel = match ctx.app.ui.dict_on_section {
|
||||||
|
Some(s) => VisibleEntry::Section(s),
|
||||||
|
None => VisibleEntry::Item(ctx.app.ui.dict_category),
|
||||||
|
};
|
||||||
|
if let Some(entry) = hit_test_visible_list(&items, &sel, cat_area, row) {
|
||||||
|
match entry {
|
||||||
|
VisibleEntry::Item(i) => ctx.dispatch(AppCommand::DictSelectCategory(i)),
|
||||||
|
VisibleEntry::Section(s) => {
|
||||||
|
let collapsed = ctx.app.ui.dict_collapsed.get(s).copied().unwrap_or(false);
|
||||||
|
if collapsed {
|
||||||
|
if let Some(v) = ctx.app.ui.dict_collapsed.get_mut(s) {
|
||||||
|
*v = false;
|
||||||
|
}
|
||||||
|
ctx.app.ui.dict_on_section = None;
|
||||||
|
if let Some(first) = categories::first_category_in_section(s) {
|
||||||
|
ctx.app.ui.dict_category = first;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if let Some(v) = ctx.app.ui.dict_collapsed.get_mut(s) {
|
||||||
|
*v = true;
|
||||||
|
}
|
||||||
|
ctx.app.ui.dict_on_section = Some(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
ctx.app.ui.dict_focus = DictFocus::Categories;
|
ctx.app.ui.dict_focus = DictFocus::Categories;
|
||||||
} else if contains(words_area, col, row) {
|
} else if contains(words_area, col, row) {
|
||||||
@@ -527,32 +578,54 @@ fn handle_dict_click(ctx: &mut InputContext, col: u16, row: u16, area: Rect) {
|
|||||||
|
|
||||||
// --- CategoryList hit test ---
|
// --- CategoryList hit test ---
|
||||||
|
|
||||||
fn hit_test_category_list(
|
#[derive(Clone, Copy)]
|
||||||
is_section: &[bool],
|
enum VisibleEntry {
|
||||||
selected: usize,
|
Item(usize),
|
||||||
|
Section(usize),
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_visible_list(is_section: &[bool], collapsed: &[bool]) -> Vec<VisibleEntry> {
|
||||||
|
let mut result = Vec::new();
|
||||||
|
let mut section_idx = 0usize;
|
||||||
|
let mut item_idx = 0usize;
|
||||||
|
let mut skipping = false;
|
||||||
|
for &is_sec in is_section {
|
||||||
|
if is_sec {
|
||||||
|
let is_collapsed = collapsed.get(section_idx).copied().unwrap_or(false);
|
||||||
|
result.push(VisibleEntry::Section(section_idx));
|
||||||
|
skipping = is_collapsed;
|
||||||
|
section_idx += 1;
|
||||||
|
} else if !skipping {
|
||||||
|
result.push(VisibleEntry::Item(item_idx));
|
||||||
|
item_idx += 1;
|
||||||
|
} else {
|
||||||
|
item_idx += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hit_test_visible_list(
|
||||||
|
items: &[VisibleEntry],
|
||||||
|
selected: &VisibleEntry,
|
||||||
area: Rect,
|
area: Rect,
|
||||||
click_row: u16,
|
click_row: u16,
|
||||||
) -> Option<usize> {
|
) -> Option<VisibleEntry> {
|
||||||
let visible_height = area.height.saturating_sub(2) as usize;
|
let visible_height = area.height.saturating_sub(2) as usize;
|
||||||
if visible_height == 0 {
|
if visible_height == 0 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let total_items = is_section.len();
|
let total_items = items.len();
|
||||||
|
|
||||||
// Compute visual index of the selected item (same as CategoryList::render)
|
let selected_visual_idx = match selected {
|
||||||
let selected_visual_idx = {
|
VisibleEntry::Item(sel) => items
|
||||||
let mut visual = 0;
|
.iter()
|
||||||
let mut selectable_count = 0;
|
.position(|v| matches!(v, VisibleEntry::Item(i) if *i == *sel))
|
||||||
for &is_sec in is_section {
|
.unwrap_or(0),
|
||||||
if !is_sec {
|
VisibleEntry::Section(sel) => items
|
||||||
if selectable_count == selected {
|
.iter()
|
||||||
break;
|
.position(|v| matches!(v, VisibleEntry::Section(s) if *s == *sel))
|
||||||
}
|
.unwrap_or(0),
|
||||||
selectable_count += 1;
|
|
||||||
}
|
|
||||||
visual += 1;
|
|
||||||
}
|
|
||||||
visual
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let scroll = if selected_visual_idx < visible_height / 2 {
|
let scroll = if selected_visual_idx < visible_height / 2 {
|
||||||
@@ -563,7 +636,6 @@ fn hit_test_category_list(
|
|||||||
selected_visual_idx.saturating_sub(visible_height / 2)
|
selected_visual_idx.saturating_sub(visible_height / 2)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Inner area starts at area.y + 1 (border top), height is area.height - 2
|
|
||||||
let inner_y = area.y + 1;
|
let inner_y = area.y + 1;
|
||||||
if click_row < inner_y {
|
if click_row < inner_y {
|
||||||
return None;
|
return None;
|
||||||
@@ -578,14 +650,7 @@ fn hit_test_category_list(
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If it's a section header, not clickable
|
Some(items[visual_idx])
|
||||||
if is_section[visual_idx] {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Count selectable items before this visual index to get the selectable index
|
|
||||||
let selectable_idx = is_section[..visual_idx].iter().filter(|&&s| !s).count();
|
|
||||||
Some(selectable_idx)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Options page ---
|
// --- Options page ---
|
||||||
|
|||||||
@@ -63,3 +63,53 @@ pub fn get_category_name(index: usize) -> &'static str {
|
|||||||
.nth(index)
|
.nth(index)
|
||||||
.unwrap_or("Unknown")
|
.unwrap_or("Unknown")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn section_count() -> usize {
|
||||||
|
CATEGORIES
|
||||||
|
.iter()
|
||||||
|
.filter(|e| matches!(e, Section(_)))
|
||||||
|
.count()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn section_index_for_category(cat_idx: usize) -> usize {
|
||||||
|
let mut section: usize = 0;
|
||||||
|
let mut cat: usize = 0;
|
||||||
|
for entry in CATEGORIES.iter() {
|
||||||
|
match entry {
|
||||||
|
Section(_) => section += 1,
|
||||||
|
Category(_) => {
|
||||||
|
if cat == cat_idx {
|
||||||
|
return section.saturating_sub(1);
|
||||||
|
}
|
||||||
|
cat += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
section.saturating_sub(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn first_category_in_section(section_idx: usize) -> Option<usize> {
|
||||||
|
let mut section: usize = 0;
|
||||||
|
let mut cat: usize = 0;
|
||||||
|
let mut in_target = false;
|
||||||
|
for entry in CATEGORIES.iter() {
|
||||||
|
match entry {
|
||||||
|
Section(_) => {
|
||||||
|
if in_target {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if section == section_idx {
|
||||||
|
in_target = true;
|
||||||
|
}
|
||||||
|
section += 1;
|
||||||
|
}
|
||||||
|
Category(_) => {
|
||||||
|
if in_target {
|
||||||
|
return Some(cat);
|
||||||
|
}
|
||||||
|
cat += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|||||||
@@ -122,6 +122,53 @@ pub fn topic_count() -> usize {
|
|||||||
DOCS.iter().filter(|e| matches!(e, Topic(_, _))).count()
|
DOCS.iter().filter(|e| matches!(e, Topic(_, _))).count()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn section_count() -> usize {
|
||||||
|
DOCS.iter().filter(|e| matches!(e, Section(_))).count()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn section_index_for_topic(topic_idx: usize) -> usize {
|
||||||
|
let mut section: usize = 0;
|
||||||
|
let mut topic: usize = 0;
|
||||||
|
for entry in DOCS.iter() {
|
||||||
|
match entry {
|
||||||
|
Section(_) => section += 1,
|
||||||
|
Topic(_, _) => {
|
||||||
|
if topic == topic_idx {
|
||||||
|
return section.saturating_sub(1);
|
||||||
|
}
|
||||||
|
topic += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
section.saturating_sub(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn first_topic_in_section(section_idx: usize) -> Option<usize> {
|
||||||
|
let mut section: usize = 0;
|
||||||
|
let mut topic: usize = 0;
|
||||||
|
let mut in_target = false;
|
||||||
|
for entry in DOCS.iter() {
|
||||||
|
match entry {
|
||||||
|
Section(_) => {
|
||||||
|
if in_target {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if section == section_idx {
|
||||||
|
in_target = true;
|
||||||
|
}
|
||||||
|
section += 1;
|
||||||
|
}
|
||||||
|
Topic(_, _) => {
|
||||||
|
if in_target {
|
||||||
|
return Some(topic);
|
||||||
|
}
|
||||||
|
topic += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_topic(index: usize) -> Option<(&'static str, &'static str)> {
|
pub fn get_topic(index: usize) -> Option<(&'static str, &'static str)> {
|
||||||
DOCS.iter()
|
DOCS.iter()
|
||||||
.filter_map(|e| match e {
|
.filter_map(|e| match e {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
use crate::model::categories;
|
use crate::model::categories::{self, CatEntry, CATEGORIES};
|
||||||
use crate::state::{DictFocus, UiState};
|
use crate::state::{DictFocus, UiState};
|
||||||
|
|
||||||
|
use CatEntry::{Category, Section};
|
||||||
|
|
||||||
pub fn toggle_focus(ui: &mut UiState) {
|
pub fn toggle_focus(ui: &mut UiState) {
|
||||||
ui.dict_focus = match ui.dict_focus {
|
ui.dict_focus = match ui.dict_focus {
|
||||||
DictFocus::Categories => DictFocus::Words,
|
DictFocus::Categories => DictFocus::Words,
|
||||||
@@ -11,17 +13,86 @@ pub fn toggle_focus(ui: &mut UiState) {
|
|||||||
pub fn select_category(ui: &mut UiState, index: usize) {
|
pub fn select_category(ui: &mut UiState, index: usize) {
|
||||||
if index < categories::category_count() {
|
if index < categories::category_count() {
|
||||||
ui.dict_category = index;
|
ui.dict_category = index;
|
||||||
|
ui.dict_on_section = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
enum VisibleItem {
|
||||||
|
Category(usize),
|
||||||
|
Section(usize),
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visible_items(collapsed: &[bool]) -> Vec<VisibleItem> {
|
||||||
|
let mut result = Vec::new();
|
||||||
|
let mut section_idx = 0usize;
|
||||||
|
let mut cat_idx = 0usize;
|
||||||
|
let mut skipping = false;
|
||||||
|
for entry in CATEGORIES.iter() {
|
||||||
|
match entry {
|
||||||
|
Section(_) => {
|
||||||
|
let is_collapsed = collapsed.get(section_idx).copied().unwrap_or(false);
|
||||||
|
if is_collapsed {
|
||||||
|
result.push(VisibleItem::Section(section_idx));
|
||||||
|
}
|
||||||
|
skipping = is_collapsed;
|
||||||
|
section_idx += 1;
|
||||||
|
}
|
||||||
|
Category(_) => {
|
||||||
|
if !skipping {
|
||||||
|
result.push(VisibleItem::Category(cat_idx));
|
||||||
|
}
|
||||||
|
cat_idx += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_current(items: &[VisibleItem], ui: &UiState) -> usize {
|
||||||
|
if let Some(s) = ui.dict_on_section {
|
||||||
|
items
|
||||||
|
.iter()
|
||||||
|
.position(|v| matches!(v, VisibleItem::Section(idx) if *idx == s))
|
||||||
|
.unwrap_or(0)
|
||||||
|
} else {
|
||||||
|
items
|
||||||
|
.iter()
|
||||||
|
.position(|v| matches!(v, VisibleItem::Category(idx) if *idx == ui.dict_category))
|
||||||
|
.unwrap_or(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_selection(ui: &mut UiState, item: VisibleItem) {
|
||||||
|
match item {
|
||||||
|
VisibleItem::Category(idx) => {
|
||||||
|
ui.dict_category = idx;
|
||||||
|
ui.dict_on_section = None;
|
||||||
|
}
|
||||||
|
VisibleItem::Section(idx) => {
|
||||||
|
ui.dict_on_section = Some(idx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn next_category(ui: &mut UiState) {
|
pub fn next_category(ui: &mut UiState) {
|
||||||
let count = categories::category_count();
|
let items = visible_items(&ui.dict_collapsed);
|
||||||
ui.dict_category = (ui.dict_category + 1) % count;
|
if items.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let cur = find_current(&items, ui);
|
||||||
|
let next = (cur + 1) % items.len();
|
||||||
|
apply_selection(ui, items[next]);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn prev_category(ui: &mut UiState) {
|
pub fn prev_category(ui: &mut UiState) {
|
||||||
let count = categories::category_count();
|
let items = visible_items(&ui.dict_collapsed);
|
||||||
ui.dict_category = (ui.dict_category + count - 1) % count;
|
if items.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let cur = find_current(&items, ui);
|
||||||
|
let next = (cur + items.len() - 1) % items.len();
|
||||||
|
apply_selection(ui, items[next]);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn scroll_down(ui: &mut UiState, n: usize) {
|
pub fn scroll_down(ui: &mut UiState, n: usize) {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
use crate::model::docs;
|
use crate::model::docs::{self, DocEntry, DOCS};
|
||||||
use crate::state::{HelpFocus, UiState};
|
use crate::state::{HelpFocus, UiState};
|
||||||
|
|
||||||
|
use DocEntry::{Section, Topic};
|
||||||
|
|
||||||
pub fn toggle_focus(ui: &mut UiState) {
|
pub fn toggle_focus(ui: &mut UiState) {
|
||||||
ui.help_focus = match ui.help_focus {
|
ui.help_focus = match ui.help_focus {
|
||||||
HelpFocus::Topics => HelpFocus::Content,
|
HelpFocus::Topics => HelpFocus::Content,
|
||||||
@@ -11,18 +13,87 @@ pub fn toggle_focus(ui: &mut UiState) {
|
|||||||
pub fn select_topic(ui: &mut UiState, index: usize) {
|
pub fn select_topic(ui: &mut UiState, index: usize) {
|
||||||
if index < docs::topic_count() {
|
if index < docs::topic_count() {
|
||||||
ui.help_topic = index;
|
ui.help_topic = index;
|
||||||
|
ui.help_on_section = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
enum VisibleItem {
|
||||||
|
Topic(usize),
|
||||||
|
Section(usize),
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visible_items(collapsed: &[bool]) -> Vec<VisibleItem> {
|
||||||
|
let mut result = Vec::new();
|
||||||
|
let mut section_idx = 0usize;
|
||||||
|
let mut topic_idx = 0usize;
|
||||||
|
let mut skipping = false;
|
||||||
|
for entry in DOCS.iter() {
|
||||||
|
match entry {
|
||||||
|
Section(_) => {
|
||||||
|
let is_collapsed = collapsed.get(section_idx).copied().unwrap_or(false);
|
||||||
|
if is_collapsed {
|
||||||
|
result.push(VisibleItem::Section(section_idx));
|
||||||
|
}
|
||||||
|
skipping = is_collapsed;
|
||||||
|
section_idx += 1;
|
||||||
|
}
|
||||||
|
Topic(_, _) => {
|
||||||
|
if !skipping {
|
||||||
|
result.push(VisibleItem::Topic(topic_idx));
|
||||||
|
}
|
||||||
|
topic_idx += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_current(items: &[VisibleItem], ui: &UiState) -> usize {
|
||||||
|
if let Some(s) = ui.help_on_section {
|
||||||
|
items
|
||||||
|
.iter()
|
||||||
|
.position(|v| matches!(v, VisibleItem::Section(idx) if *idx == s))
|
||||||
|
.unwrap_or(0)
|
||||||
|
} else {
|
||||||
|
items
|
||||||
|
.iter()
|
||||||
|
.position(|v| matches!(v, VisibleItem::Topic(idx) if *idx == ui.help_topic))
|
||||||
|
.unwrap_or(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_selection(ui: &mut UiState, item: VisibleItem) {
|
||||||
|
match item {
|
||||||
|
VisibleItem::Topic(idx) => {
|
||||||
|
ui.help_topic = idx;
|
||||||
|
ui.help_on_section = None;
|
||||||
|
}
|
||||||
|
VisibleItem::Section(idx) => {
|
||||||
|
ui.help_on_section = Some(idx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn next_topic(ui: &mut UiState, n: usize) {
|
pub fn next_topic(ui: &mut UiState, n: usize) {
|
||||||
let count = docs::topic_count();
|
let items = visible_items(&ui.help_collapsed);
|
||||||
ui.help_topic = (ui.help_topic + n) % count;
|
if items.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let cur = find_current(&items, ui);
|
||||||
|
let next = (cur + n) % items.len();
|
||||||
|
apply_selection(ui, items[next]);
|
||||||
ui.help_focused_block = None;
|
ui.help_focused_block = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn prev_topic(ui: &mut UiState, n: usize) {
|
pub fn prev_topic(ui: &mut UiState, n: usize) {
|
||||||
let count = docs::topic_count();
|
let items = visible_items(&ui.help_collapsed);
|
||||||
ui.help_topic = (ui.help_topic + count - (n % count)) % count;
|
if items.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let cur = find_current(&items, ui);
|
||||||
|
let next = (cur + items.len() - (n % items.len())) % items.len();
|
||||||
|
apply_selection(ui, items[next]);
|
||||||
ui.help_focused_block = None;
|
ui.help_focused_block = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,6 +120,7 @@ pub fn search_input(ui: &mut UiState, c: char) {
|
|||||||
ui.help_search_query.push(c);
|
ui.help_search_query.push(c);
|
||||||
if let Some((topic, line)) = docs::find_match(&ui.help_search_query) {
|
if let Some((topic, line)) = docs::find_match(&ui.help_search_query) {
|
||||||
ui.help_topic = topic;
|
ui.help_topic = topic;
|
||||||
|
ui.help_on_section = None;
|
||||||
ui.help_scrolls[topic] = line;
|
ui.help_scrolls[topic] = line;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -60,6 +132,7 @@ pub fn search_backspace(ui: &mut UiState) {
|
|||||||
}
|
}
|
||||||
if let Some((topic, line)) = docs::find_match(&ui.help_search_query) {
|
if let Some((topic, line)) = docs::find_match(&ui.help_search_query) {
|
||||||
ui.help_topic = topic;
|
ui.help_topic = topic;
|
||||||
|
ui.help_on_section = None;
|
||||||
ui.help_scrolls[topic] = line;
|
ui.help_scrolls[topic] = line;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,6 +57,10 @@ pub struct UiState {
|
|||||||
pub dict_scrolls: Vec<usize>,
|
pub dict_scrolls: Vec<usize>,
|
||||||
pub dict_search_query: String,
|
pub dict_search_query: String,
|
||||||
pub dict_search_active: bool,
|
pub dict_search_active: bool,
|
||||||
|
pub help_collapsed: Vec<bool>,
|
||||||
|
pub help_on_section: Option<usize>,
|
||||||
|
pub dict_collapsed: Vec<bool>,
|
||||||
|
pub dict_on_section: Option<usize>,
|
||||||
pub show_title: bool,
|
pub show_title: bool,
|
||||||
pub runtime_highlight: bool,
|
pub runtime_highlight: bool,
|
||||||
pub show_completion: bool,
|
pub show_completion: bool,
|
||||||
@@ -86,12 +90,20 @@ impl Default for UiState {
|
|||||||
help_search_active: false,
|
help_search_active: false,
|
||||||
help_search_query: String::new(),
|
help_search_query: String::new(),
|
||||||
help_focused_block: None,
|
help_focused_block: None,
|
||||||
help_parsed: RefCell::new((0..crate::model::docs::topic_count()).map(|_| None).collect()),
|
help_parsed: RefCell::new(
|
||||||
|
(0..crate::model::docs::topic_count())
|
||||||
|
.map(|_| None)
|
||||||
|
.collect(),
|
||||||
|
),
|
||||||
dict_focus: DictFocus::default(),
|
dict_focus: DictFocus::default(),
|
||||||
dict_category: 0,
|
dict_category: 0,
|
||||||
dict_scrolls: vec![0; crate::model::categories::category_count()],
|
dict_scrolls: vec![0; crate::model::categories::category_count()],
|
||||||
dict_search_query: String::new(),
|
dict_search_query: String::new(),
|
||||||
dict_search_active: false,
|
dict_search_active: false,
|
||||||
|
help_collapsed: vec![false; crate::model::docs::section_count()],
|
||||||
|
help_on_section: None,
|
||||||
|
dict_collapsed: vec![false; crate::model::categories::section_count()],
|
||||||
|
dict_on_section: None,
|
||||||
show_title: true,
|
show_title: true,
|
||||||
runtime_highlight: false,
|
runtime_highlight: false,
|
||||||
show_completion: true,
|
show_completion: true,
|
||||||
@@ -167,6 +179,9 @@ impl UiState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn invalidate_help_cache(&self) {
|
pub fn invalidate_help_cache(&self) {
|
||||||
self.help_parsed.borrow_mut().iter_mut().for_each(|slot| *slot = None);
|
self.help_parsed
|
||||||
|
.borrow_mut()
|
||||||
|
.iter_mut()
|
||||||
|
.for_each(|slot| *slot = None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use crate::model::categories::{get_category_name, CatEntry, CATEGORIES};
|
|||||||
use crate::model::{Word, WORDS};
|
use crate::model::{Word, WORDS};
|
||||||
use crate::state::DictFocus;
|
use crate::state::DictFocus;
|
||||||
use crate::theme;
|
use crate::theme;
|
||||||
use crate::widgets::{render_search_bar, CategoryItem, CategoryList};
|
use crate::widgets::{render_search_bar, CategoryItem, CategoryList, Selection};
|
||||||
|
|
||||||
use CatEntry::{Category, Section};
|
use CatEntry::{Category, Section};
|
||||||
|
|
||||||
@@ -51,21 +51,38 @@ fn render_categories(frame: &mut Frame, app: &App, area: Rect, dimmed: bool) {
|
|||||||
let theme = theme::get();
|
let theme = theme::get();
|
||||||
let focused = app.ui.dict_focus == DictFocus::Categories && !dimmed;
|
let focused = app.ui.dict_focus == DictFocus::Categories && !dimmed;
|
||||||
|
|
||||||
|
let mut section_idx = 0usize;
|
||||||
let items: Vec<CategoryItem> = CATEGORIES
|
let items: Vec<CategoryItem> = CATEGORIES
|
||||||
.iter()
|
.iter()
|
||||||
.map(|entry| match entry {
|
.map(|entry| match entry {
|
||||||
Section(name) => CategoryItem {
|
Section(name) => {
|
||||||
|
let collapsed = app
|
||||||
|
.ui
|
||||||
|
.dict_collapsed
|
||||||
|
.get(section_idx)
|
||||||
|
.copied()
|
||||||
|
.unwrap_or(false);
|
||||||
|
section_idx += 1;
|
||||||
|
CategoryItem {
|
||||||
label: name,
|
label: name,
|
||||||
is_section: true,
|
is_section: true,
|
||||||
},
|
collapsed,
|
||||||
|
}
|
||||||
|
}
|
||||||
Category(name) => CategoryItem {
|
Category(name) => CategoryItem {
|
||||||
label: name,
|
label: name,
|
||||||
is_section: false,
|
is_section: false,
|
||||||
|
collapsed: false,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let mut list = CategoryList::new(&items, app.ui.dict_category)
|
let selection = match app.ui.dict_on_section {
|
||||||
|
Some(s) => Selection::Section(s),
|
||||||
|
None => Selection::Item(app.ui.dict_category),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut list = CategoryList::new(&items, selection)
|
||||||
.focused(focused)
|
.focused(focused)
|
||||||
.title("Categories");
|
.title("Categories");
|
||||||
|
|
||||||
@@ -91,23 +108,24 @@ fn render_words(frame: &mut Frame, app: &App, area: Rect, is_searching: bool) {
|
|||||||
.collect()
|
.collect()
|
||||||
} else {
|
} else {
|
||||||
let category = get_category_name(app.ui.dict_category);
|
let category = get_category_name(app.ui.dict_category);
|
||||||
WORDS
|
WORDS.iter().filter(|w| w.category == category).collect()
|
||||||
.iter()
|
|
||||||
.filter(|w| w.category == category)
|
|
||||||
.collect()
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let show_search = app.ui.dict_search_active || is_searching;
|
let show_search = app.ui.dict_search_active || is_searching;
|
||||||
let (search_area, content_area) = if show_search {
|
let (search_area, content_area) = if show_search {
|
||||||
let [s, c] =
|
let [s, c] = Layout::vertical([Constraint::Length(1), Constraint::Fill(1)]).areas(area);
|
||||||
Layout::vertical([Constraint::Length(1), Constraint::Fill(1)]).areas(area);
|
|
||||||
(Some(s), c)
|
(Some(s), c)
|
||||||
} else {
|
} else {
|
||||||
(None, area)
|
(None, area)
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(sa) = search_area {
|
if let Some(sa) = search_area {
|
||||||
render_search_bar(frame, sa, &app.ui.dict_search_query, app.ui.dict_search_active);
|
render_search_bar(
|
||||||
|
frame,
|
||||||
|
sa,
|
||||||
|
&app.ui.dict_search_query,
|
||||||
|
app.ui.dict_search_active,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let content_width = content_area.width.saturating_sub(2) as usize;
|
let content_width = content_area.width.saturating_sub(2) as usize;
|
||||||
@@ -172,7 +190,11 @@ fn render_words(frame: &mut Frame, app: &App, area: Rect, is_searching: bool) {
|
|||||||
let category = get_category_name(app.ui.dict_category);
|
let category = get_category_name(app.ui.dict_category);
|
||||||
format!("{category} ({} words)", words.len())
|
format!("{category} ({} words)", words.len())
|
||||||
};
|
};
|
||||||
let border_color = if focused { theme.dict.border_focused } else { theme.dict.border_normal };
|
let border_color = if focused {
|
||||||
|
theme.dict.border_focused
|
||||||
|
} else {
|
||||||
|
theme.dict.border_normal
|
||||||
|
};
|
||||||
let block = Block::default()
|
let block = Block::default()
|
||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
.border_style(Style::new().fg(border_color))
|
.border_style(Style::new().fg(border_color))
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ use crate::model::docs::{get_topic, DocEntry, DOCS};
|
|||||||
use crate::state::HelpFocus;
|
use crate::state::HelpFocus;
|
||||||
use crate::theme;
|
use crate::theme;
|
||||||
use crate::views::highlight;
|
use crate::views::highlight;
|
||||||
use crate::widgets::{render_search_bar, CategoryItem, CategoryList};
|
use crate::widgets::{render_search_bar, CategoryItem, CategoryList, Selection};
|
||||||
|
|
||||||
use DocEntry::{Section, Topic};
|
use DocEntry::{Section, Topic};
|
||||||
|
|
||||||
@@ -105,21 +105,38 @@ fn render_topics(frame: &mut Frame, app: &App, area: Rect) {
|
|||||||
let theme = theme::get();
|
let theme = theme::get();
|
||||||
let focused = app.ui.help_focus == HelpFocus::Topics;
|
let focused = app.ui.help_focus == HelpFocus::Topics;
|
||||||
|
|
||||||
|
let mut section_idx = 0usize;
|
||||||
let items: Vec<CategoryItem> = DOCS
|
let items: Vec<CategoryItem> = DOCS
|
||||||
.iter()
|
.iter()
|
||||||
.map(|entry| match entry {
|
.map(|entry| match entry {
|
||||||
Section(name) => CategoryItem {
|
Section(name) => {
|
||||||
|
let collapsed = app
|
||||||
|
.ui
|
||||||
|
.help_collapsed
|
||||||
|
.get(section_idx)
|
||||||
|
.copied()
|
||||||
|
.unwrap_or(false);
|
||||||
|
section_idx += 1;
|
||||||
|
CategoryItem {
|
||||||
label: name,
|
label: name,
|
||||||
is_section: true,
|
is_section: true,
|
||||||
},
|
collapsed,
|
||||||
|
}
|
||||||
|
}
|
||||||
Topic(name, _) => CategoryItem {
|
Topic(name, _) => CategoryItem {
|
||||||
label: name,
|
label: name,
|
||||||
is_section: false,
|
is_section: false,
|
||||||
|
collapsed: false,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
CategoryList::new(&items, app.ui.help_topic)
|
let selection = match app.ui.help_on_section {
|
||||||
|
Some(s) => Selection::Section(s),
|
||||||
|
None => Selection::Item(app.ui.help_topic),
|
||||||
|
};
|
||||||
|
|
||||||
|
CategoryList::new(&items, selection)
|
||||||
.focused(focused)
|
.focused(focused)
|
||||||
.title("Topics")
|
.title("Topics")
|
||||||
.selected_color(theme.dict.category_selected)
|
.selected_color(theme.dict.category_selected)
|
||||||
@@ -172,7 +189,8 @@ fn render_content(frame: &mut Frame, app: &App, area: Rect) {
|
|||||||
{
|
{
|
||||||
let mut cache = app.ui.help_parsed.borrow_mut();
|
let mut cache = app.ui.help_parsed.borrow_mut();
|
||||||
if cache[app.ui.help_topic].is_none() {
|
if cache[app.ui.help_topic].is_none() {
|
||||||
cache[app.ui.help_topic] = Some(cagire_markdown::parse(md, &AppTheme, &ForthHighlighter));
|
cache[app.ui.help_topic] =
|
||||||
|
Some(cagire_markdown::parse(md, &AppTheme, &ForthHighlighter));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let cache = app.ui.help_parsed.borrow();
|
let cache = app.ui.help_parsed.borrow();
|
||||||
@@ -182,7 +200,12 @@ fn render_content(frame: &mut Frame, app: &App, area: Rect) {
|
|||||||
let content_area = if has_search_bar {
|
let content_area = if has_search_bar {
|
||||||
let [content, search] =
|
let [content, search] =
|
||||||
Layout::vertical([Constraint::Fill(1), Constraint::Length(1)]).areas(md_area);
|
Layout::vertical([Constraint::Fill(1), Constraint::Length(1)]).areas(md_area);
|
||||||
render_search_bar(frame, search, &app.ui.help_search_query, app.ui.help_search_active);
|
render_search_bar(
|
||||||
|
frame,
|
||||||
|
search,
|
||||||
|
&app.ui.help_search_query,
|
||||||
|
app.ui.help_search_active,
|
||||||
|
);
|
||||||
content
|
content
|
||||||
} else {
|
} else {
|
||||||
md_area
|
md_area
|
||||||
@@ -295,4 +318,3 @@ fn highlight_line<'a>(line: RLine<'a>, query: &str) -> RLine<'a> {
|
|||||||
fn find_bytes(haystack: &[u8], needle: &[u8]) -> Option<usize> {
|
fn find_bytes(haystack: &[u8], needle: &[u8]) -> Option<usize> {
|
||||||
haystack.windows(needle.len()).position(|w| w == needle)
|
haystack.windows(needle.len()).position(|w| w == needle)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,5 +2,5 @@ pub use cagire_ratatui::{
|
|||||||
hint_line, render_props_form, render_scroll_indicators, render_search_bar,
|
hint_line, render_props_form, render_scroll_indicators, render_search_bar,
|
||||||
render_section_header, ActivePatterns, CategoryItem, CategoryList, ConfirmModal,
|
render_section_header, ActivePatterns, CategoryItem, CategoryList, ConfirmModal,
|
||||||
FileBrowserModal, IndicatorAlign, ModalFrame, MuteStatus, NavMinimap, NavTile, Orientation,
|
FileBrowserModal, IndicatorAlign, ModalFrame, MuteStatus, NavMinimap, NavTile, Orientation,
|
||||||
SampleBrowser, Scope, Spectrum, TextInputModal, VuMeter, Waveform,
|
SampleBrowser, Scope, Selection, Spectrum, TextInputModal, VuMeter, Waveform,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user