Feat: collapsible help
This commit is contained in:
@@ -4,8 +4,8 @@ use ratatui::layout::{Constraint, Layout, Rect};
|
||||
use crate::commands::AppCommand;
|
||||
use crate::page::Page;
|
||||
use crate::state::{
|
||||
DictFocus, DeviceKind, EngineSection, HelpFocus, MainLayout, MinimapMode, Modal,
|
||||
OptionsFocus, PatternsColumn, SettingKind,
|
||||
DeviceKind, DictFocus, EngineSection, HelpFocus, MainLayout, MinimapMode, Modal, OptionsFocus,
|
||||
PatternsColumn, SettingKind,
|
||||
};
|
||||
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 {
|
||||
col >= area.x
|
||||
&& col < area.x + area.width
|
||||
&& row >= area.y
|
||||
&& row < area.y + area.height
|
||||
col >= area.x && 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) {
|
||||
@@ -319,20 +316,16 @@ fn handle_main_click(ctx: &mut InputContext, col: u16, row: u16, area: Rect) {
|
||||
let sequencer_area = match layout {
|
||||
MainLayout::Top => {
|
||||
let viz_height = if has_viz { 16 } else { 0 };
|
||||
let [_viz, seq] = Layout::vertical([
|
||||
Constraint::Length(viz_height),
|
||||
Constraint::Fill(1),
|
||||
])
|
||||
.areas(main_area);
|
||||
let [_viz, seq] =
|
||||
Layout::vertical([Constraint::Length(viz_height), Constraint::Fill(1)])
|
||||
.areas(main_area);
|
||||
seq
|
||||
}
|
||||
MainLayout::Bottom => {
|
||||
let viz_height = if has_viz { 16 } else { 0 };
|
||||
let [seq, _viz] = Layout::vertical([
|
||||
Constraint::Fill(1),
|
||||
Constraint::Length(viz_height),
|
||||
])
|
||||
.areas(main_area);
|
||||
let [seq, _viz] =
|
||||
Layout::vertical([Constraint::Fill(1), Constraint::Length(viz_height)])
|
||||
.areas(main_area);
|
||||
seq
|
||||
}
|
||||
MainLayout::Left => {
|
||||
@@ -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);
|
||||
|
||||
if contains(topics_area, col, row) {
|
||||
use crate::model::docs::{DocEntry, DOCS};
|
||||
let is_section: Vec<bool> = DOCS.iter().map(|e| matches!(e, DocEntry::Section(_))).collect();
|
||||
if let Some(i) = hit_test_category_list(&is_section, ctx.app.ui.help_topic, topics_area, row) {
|
||||
ctx.dispatch(AppCommand::HelpSelectTopic(i));
|
||||
use crate::model::docs::{self, DocEntry, DOCS};
|
||||
let items = build_visible_list(
|
||||
&DOCS
|
||||
.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;
|
||||
} 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);
|
||||
|
||||
if contains(cat_area, col, row) {
|
||||
use crate::model::categories::{CatEntry, CATEGORIES};
|
||||
let is_section: Vec<bool> = CATEGORIES.iter().map(|e| matches!(e, CatEntry::Section(_))).collect();
|
||||
if let Some(i) = hit_test_category_list(&is_section, ctx.app.ui.dict_category, cat_area, row) {
|
||||
ctx.dispatch(AppCommand::DictSelectCategory(i));
|
||||
use crate::model::categories::{self, CatEntry, CATEGORIES};
|
||||
let items = build_visible_list(
|
||||
&CATEGORIES
|
||||
.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;
|
||||
} 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 ---
|
||||
|
||||
fn hit_test_category_list(
|
||||
is_section: &[bool],
|
||||
selected: usize,
|
||||
#[derive(Clone, Copy)]
|
||||
enum VisibleEntry {
|
||||
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,
|
||||
click_row: u16,
|
||||
) -> Option<usize> {
|
||||
) -> Option<VisibleEntry> {
|
||||
let visible_height = area.height.saturating_sub(2) as usize;
|
||||
if visible_height == 0 {
|
||||
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 = {
|
||||
let mut visual = 0;
|
||||
let mut selectable_count = 0;
|
||||
for &is_sec in is_section {
|
||||
if !is_sec {
|
||||
if selectable_count == selected {
|
||||
break;
|
||||
}
|
||||
selectable_count += 1;
|
||||
}
|
||||
visual += 1;
|
||||
}
|
||||
visual
|
||||
let selected_visual_idx = match selected {
|
||||
VisibleEntry::Item(sel) => items
|
||||
.iter()
|
||||
.position(|v| matches!(v, VisibleEntry::Item(i) if *i == *sel))
|
||||
.unwrap_or(0),
|
||||
VisibleEntry::Section(sel) => items
|
||||
.iter()
|
||||
.position(|v| matches!(v, VisibleEntry::Section(s) if *s == *sel))
|
||||
.unwrap_or(0),
|
||||
};
|
||||
|
||||
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)
|
||||
};
|
||||
|
||||
// Inner area starts at area.y + 1 (border top), height is area.height - 2
|
||||
let inner_y = area.y + 1;
|
||||
if click_row < inner_y {
|
||||
return None;
|
||||
@@ -578,14 +650,7 @@ fn hit_test_category_list(
|
||||
return None;
|
||||
}
|
||||
|
||||
// If it's a section header, not clickable
|
||||
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)
|
||||
Some(items[visual_idx])
|
||||
}
|
||||
|
||||
// --- Options page ---
|
||||
|
||||
Reference in New Issue
Block a user