Files
Cagire/src/views/dict_view.rs

207 lines
6.9 KiB
Rust

use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::style::{Modifier, Style};
use ratatui::text::{Line as RLine, Span};
use ratatui::widgets::{Block, Borders, Paragraph};
use ratatui::Frame;
use crate::app::App;
use crate::model::categories::{get_category_name, CatEntry, CATEGORIES};
use crate::model::{Word, WORDS};
use crate::state::DictFocus;
use crate::theme;
use crate::widgets::{render_search_bar, CategoryItem, CategoryList, Selection};
use CatEntry::{Category, Section};
pub fn layout(area: Rect) -> (Rect, [Rect; 2]) {
let [header_area, body_area] =
Layout::vertical([Constraint::Length(5), Constraint::Fill(1)]).areas(area);
let body = Layout::horizontal([Constraint::Length(16), Constraint::Fill(1)]).areas(body_area);
(header_area, body)
}
pub fn render(frame: &mut Frame, app: &App, area: Rect) {
let (header_area, [cat_area, words_area]) = layout(area);
render_header(frame, header_area);
let is_searching = !app.ui.dict_search_query.is_empty();
render_categories(frame, app, cat_area, is_searching);
render_words(frame, app, words_area, is_searching);
}
fn render_header(frame: &mut Frame, area: Rect) {
use ratatui::widgets::Wrap;
let theme = theme::get();
let desc = "Forth uses a stack: values are pushed, functions (called words) consume and \
produce values. Read left to right: 3 4 + -> push 3, push 4, + pops both, \
pushes 7. This page lists all words with their signature ( inputs -- outputs ).";
let block = Block::default()
.borders(Borders::ALL)
.border_style(Style::new().fg(theme.dict.border_normal))
.title("Dictionary");
let para = Paragraph::new(desc)
.style(Style::new().fg(theme.dict.header_desc))
.wrap(Wrap { trim: false })
.block(block);
frame.render_widget(para, area);
}
fn render_categories(frame: &mut Frame, app: &App, area: Rect, dimmed: bool) {
let theme = theme::get();
let focused = app.ui.dict_focus == DictFocus::Categories && !dimmed;
let mut section_idx = 0usize;
let items: Vec<CategoryItem> = CATEGORIES
.iter()
.map(|entry| match entry {
Section(name) => {
let collapsed = app
.ui
.dict_collapsed
.get(section_idx)
.copied()
.unwrap_or(false);
section_idx += 1;
CategoryItem {
label: name,
is_section: true,
collapsed,
}
}
Category(name) => CategoryItem {
label: name,
is_section: false,
collapsed: false,
},
})
.collect();
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)
.title("Categories");
if dimmed {
list = list.dimmed(theme.dict.category_dimmed);
}
list.render(frame, area);
}
fn render_words(frame: &mut Frame, app: &App, area: Rect, is_searching: bool) {
let theme = theme::get();
let focused = app.ui.dict_focus == DictFocus::Words;
let words: Vec<&Word> = if is_searching {
let query = app.ui.dict_search_query.to_lowercase();
WORDS
.iter()
.filter(|w| {
w.name.to_lowercase().contains(&query)
|| w.aliases.iter().any(|a| a.to_lowercase().contains(&query))
})
.collect()
} else {
let category = get_category_name(app.ui.dict_category);
WORDS.iter().filter(|w| w.category == category).collect()
};
let show_search = app.ui.dict_search_active || is_searching;
let (search_area, content_area) = if show_search {
let [s, c] = Layout::vertical([Constraint::Length(1), Constraint::Fill(1)]).areas(area);
(Some(s), c)
} else {
(None, area)
};
if let Some(sa) = search_area {
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 mut lines: Vec<RLine> = Vec::new();
for word in &words {
let name_bg = theme.dict.word_bg;
let name_style = Style::new()
.fg(theme.dict.word_name)
.bg(name_bg)
.add_modifier(Modifier::BOLD);
let alias_style = Style::new().fg(theme.dict.alias).bg(name_bg);
let name_text = if word.aliases.is_empty() {
format!(" {}", word.name)
} else {
format!(" {} ({})", word.name, word.aliases.join(", "))
};
let padding = " ".repeat(content_width.saturating_sub(name_text.chars().count()));
if word.aliases.is_empty() {
lines.push(RLine::from(Span::styled(
format!("{name_text}{padding}"),
name_style,
)));
} else {
lines.push(RLine::from(vec![
Span::styled(format!(" {}", word.name), name_style),
Span::styled(format!(" ({})", word.aliases.join(", ")), alias_style),
Span::styled(padding, name_style),
]));
}
let stack_style = Style::new().fg(theme.dict.stack_sig);
lines.push(RLine::from(vec![
Span::raw(" "),
Span::styled(word.stack.to_string(), stack_style),
]));
let desc_style = Style::new().fg(theme.dict.description);
lines.push(RLine::from(vec![
Span::raw(" "),
Span::styled(word.desc.to_string(), desc_style),
]));
let example_style = Style::new().fg(theme.dict.example);
lines.push(RLine::from(vec![
Span::raw(" "),
Span::styled(format!("e.g. {}", word.example), example_style),
]));
lines.push(RLine::from(""));
}
let visible_height = content_area.height.saturating_sub(2) as usize;
let total_lines = lines.len();
let max_scroll = total_lines.saturating_sub(visible_height);
let scroll = app.ui.dict_scroll().min(max_scroll);
let title = if is_searching {
format!("Search: {} matches", words.len())
} else {
let category = get_category_name(app.ui.dict_category);
format!("{category} ({} words)", words.len())
};
let border_color = if focused {
theme.dict.border_focused
} else {
theme.dict.border_normal
};
let block = Block::default()
.borders(Borders::ALL)
.border_style(Style::new().fg(border_color))
.title(title);
let para = Paragraph::new(lines)
.scroll((scroll as u16, 0))
.block(block);
frame.render_widget(para, content_area);
}