WIP: menu
This commit is contained in:
@@ -3,6 +3,7 @@ mod editor;
|
||||
mod file_browser;
|
||||
mod list_select;
|
||||
mod modal;
|
||||
mod nav_minimap;
|
||||
mod sample_browser;
|
||||
mod scope;
|
||||
mod spectrum;
|
||||
@@ -14,6 +15,7 @@ pub use editor::{CompletionCandidate, Editor};
|
||||
pub use file_browser::FileBrowserModal;
|
||||
pub use list_select::ListSelect;
|
||||
pub use modal::ModalFrame;
|
||||
pub use nav_minimap::{NavMinimap, NavTile};
|
||||
pub use sample_browser::{SampleBrowser, TreeLine, TreeLineKind};
|
||||
pub use scope::{Orientation, Scope};
|
||||
pub use spectrum::Spectrum;
|
||||
|
||||
86
crates/ratatui/src/nav_minimap.rs
Normal file
86
crates/ratatui/src/nav_minimap.rs
Normal file
@@ -0,0 +1,86 @@
|
||||
use ratatui::layout::{Alignment, Rect};
|
||||
use ratatui::style::{Color, Style};
|
||||
use ratatui::widgets::{Clear, Paragraph};
|
||||
use ratatui::Frame;
|
||||
|
||||
/// A tile in the navigation grid
|
||||
pub struct NavTile {
|
||||
pub col: i8,
|
||||
pub row: i8,
|
||||
pub name: &'static str,
|
||||
}
|
||||
|
||||
/// Navigation minimap widget that renders a grid of page tiles
|
||||
pub struct NavMinimap<'a> {
|
||||
tiles: &'a [NavTile],
|
||||
selected: (i8, i8),
|
||||
}
|
||||
|
||||
impl<'a> NavMinimap<'a> {
|
||||
pub fn new(tiles: &'a [NavTile], selected: (i8, i8)) -> Self {
|
||||
Self { tiles, selected }
|
||||
}
|
||||
|
||||
pub fn render_centered(self, frame: &mut Frame, term: Rect) {
|
||||
if self.tiles.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Compute grid bounds from tiles
|
||||
let max_col = self.tiles.iter().map(|t| t.col).max().unwrap_or(0);
|
||||
let max_row = self.tiles.iter().map(|t| t.row).max().unwrap_or(0);
|
||||
let cols = (max_col + 1) as u16;
|
||||
let rows = (max_row + 1) as u16;
|
||||
|
||||
let tile_w: u16 = 12;
|
||||
let tile_h: u16 = 3;
|
||||
let gap: u16 = 1;
|
||||
let pad: u16 = 2;
|
||||
|
||||
let content_w = tile_w * cols + gap * (cols.saturating_sub(1));
|
||||
let content_h = tile_h * rows + gap * (rows.saturating_sub(1));
|
||||
let modal_w = content_w + pad * 2;
|
||||
let modal_h = content_h + pad * 2;
|
||||
|
||||
let x = term.x + (term.width.saturating_sub(modal_w)) / 2;
|
||||
let y = term.y + (term.height.saturating_sub(modal_h)) / 2;
|
||||
let area = Rect::new(x, y, modal_w, modal_h);
|
||||
|
||||
frame.render_widget(Clear, area);
|
||||
|
||||
let inner_x = area.x + pad;
|
||||
let inner_y = area.y + pad;
|
||||
|
||||
for tile in self.tiles {
|
||||
let tile_x = inner_x + (tile.col as u16) * (tile_w + gap);
|
||||
let tile_y = inner_y + (tile.row as u16) * (tile_h + gap);
|
||||
let tile_area = Rect::new(tile_x, tile_y, tile_w, tile_h);
|
||||
let is_selected = (tile.col, tile.row) == self.selected;
|
||||
self.render_tile(frame, tile_area, tile.name, is_selected);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_tile(&self, frame: &mut Frame, area: Rect, label: &str, is_selected: bool) {
|
||||
let (bg, fg) = if is_selected {
|
||||
(Color::Rgb(50, 90, 110), Color::White)
|
||||
} else {
|
||||
(Color::Rgb(30, 35, 45), Color::Rgb(100, 105, 115))
|
||||
};
|
||||
|
||||
// Fill background
|
||||
for row in 0..area.height {
|
||||
let line_area = Rect::new(area.x, area.y + row, area.width, 1);
|
||||
let fill = " ".repeat(area.width as usize);
|
||||
frame.render_widget(Paragraph::new(fill).style(Style::new().bg(bg)), line_area);
|
||||
}
|
||||
|
||||
// Center text vertically
|
||||
let text_y = area.y + area.height / 2;
|
||||
let text_area = Rect::new(area.x, text_y, area.width, 1);
|
||||
let paragraph = Paragraph::new(label)
|
||||
.style(Style::new().bg(bg).fg(fg))
|
||||
.alignment(Alignment::Center);
|
||||
|
||||
frame.render_widget(paragraph, text_area);
|
||||
}
|
||||
}
|
||||
59
src/app.rs
59
src/app.rs
@@ -19,7 +19,7 @@ use crate::state::{
|
||||
AudioSettings, EditorContext, Focus, LiveKeyState, Metrics, Modal, PanelState, PatternField,
|
||||
PatternsNav, PlaybackState, ProjectState, UiState,
|
||||
};
|
||||
use crate::views::doc_view;
|
||||
use crate::views::{dict_view, help_view};
|
||||
|
||||
const STEPS_PER_PAGE: usize = 32;
|
||||
|
||||
@@ -865,33 +865,46 @@ impl App {
|
||||
AppCommand::PageUp => self.page.up(),
|
||||
AppCommand::PageDown => self.page.down(),
|
||||
|
||||
// Doc navigation
|
||||
AppCommand::DocNextTopic => {
|
||||
self.ui.doc_topic = (self.ui.doc_topic + 1) % doc_view::topic_count();
|
||||
self.ui.doc_scroll = 0;
|
||||
self.ui.doc_category = 0;
|
||||
// Help navigation
|
||||
AppCommand::HelpNextTopic => {
|
||||
self.ui.help_topic = (self.ui.help_topic + 1) % help_view::topic_count();
|
||||
self.ui.help_scroll = 0;
|
||||
}
|
||||
AppCommand::DocPrevTopic => {
|
||||
let count = doc_view::topic_count();
|
||||
self.ui.doc_topic = (self.ui.doc_topic + count - 1) % count;
|
||||
self.ui.doc_scroll = 0;
|
||||
self.ui.doc_category = 0;
|
||||
AppCommand::HelpPrevTopic => {
|
||||
let count = help_view::topic_count();
|
||||
self.ui.help_topic = (self.ui.help_topic + count - 1) % count;
|
||||
self.ui.help_scroll = 0;
|
||||
}
|
||||
AppCommand::DocScrollDown(n) => {
|
||||
self.ui.doc_scroll = self.ui.doc_scroll.saturating_add(n);
|
||||
AppCommand::HelpScrollDown(n) => {
|
||||
self.ui.help_scroll = self.ui.help_scroll.saturating_add(n);
|
||||
}
|
||||
AppCommand::DocScrollUp(n) => {
|
||||
self.ui.doc_scroll = self.ui.doc_scroll.saturating_sub(n);
|
||||
AppCommand::HelpScrollUp(n) => {
|
||||
self.ui.help_scroll = self.ui.help_scroll.saturating_sub(n);
|
||||
}
|
||||
AppCommand::DocNextCategory => {
|
||||
let count = doc_view::category_count();
|
||||
self.ui.doc_category = (self.ui.doc_category + 1) % count;
|
||||
self.ui.doc_scroll = 0;
|
||||
|
||||
// Dictionary navigation
|
||||
AppCommand::DictToggleFocus => {
|
||||
use crate::state::DictFocus;
|
||||
self.ui.dict_focus = match self.ui.dict_focus {
|
||||
DictFocus::Categories => DictFocus::Words,
|
||||
DictFocus::Words => DictFocus::Categories,
|
||||
};
|
||||
}
|
||||
AppCommand::DocPrevCategory => {
|
||||
let count = doc_view::category_count();
|
||||
self.ui.doc_category = (self.ui.doc_category + count - 1) % count;
|
||||
self.ui.doc_scroll = 0;
|
||||
AppCommand::DictNextCategory => {
|
||||
let count = dict_view::category_count();
|
||||
self.ui.dict_category = (self.ui.dict_category + 1) % count;
|
||||
self.ui.dict_scroll = 0;
|
||||
}
|
||||
AppCommand::DictPrevCategory => {
|
||||
let count = dict_view::category_count();
|
||||
self.ui.dict_category = (self.ui.dict_category + count - 1) % count;
|
||||
self.ui.dict_scroll = 0;
|
||||
}
|
||||
AppCommand::DictScrollDown(n) => {
|
||||
self.ui.dict_scroll = self.ui.dict_scroll.saturating_add(n);
|
||||
}
|
||||
AppCommand::DictScrollUp(n) => {
|
||||
self.ui.dict_scroll = self.ui.dict_scroll.saturating_sub(n);
|
||||
}
|
||||
|
||||
// Patterns view
|
||||
|
||||
@@ -114,13 +114,18 @@ pub enum AppCommand {
|
||||
PageUp,
|
||||
PageDown,
|
||||
|
||||
// Doc navigation
|
||||
DocNextTopic,
|
||||
DocPrevTopic,
|
||||
DocScrollDown(usize),
|
||||
DocScrollUp(usize),
|
||||
DocNextCategory,
|
||||
DocPrevCategory,
|
||||
// Help navigation
|
||||
HelpNextTopic,
|
||||
HelpPrevTopic,
|
||||
HelpScrollDown(usize),
|
||||
HelpScrollUp(usize),
|
||||
|
||||
// Dictionary navigation
|
||||
DictToggleFocus,
|
||||
DictNextCategory,
|
||||
DictPrevCategory,
|
||||
DictScrollDown(usize),
|
||||
DictScrollUp(usize),
|
||||
|
||||
// Patterns view
|
||||
PatternsCursorLeft,
|
||||
|
||||
63
src/input.rs
63
src/input.rs
@@ -2,6 +2,7 @@ use crossbeam_channel::Sender;
|
||||
use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use crate::app::App;
|
||||
use crate::commands::AppCommand;
|
||||
@@ -38,6 +39,15 @@ pub fn handle_key(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||
return InputResult::Continue;
|
||||
}
|
||||
|
||||
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
|
||||
let is_arrow = matches!(
|
||||
key.code,
|
||||
KeyCode::Left | KeyCode::Right | KeyCode::Up | KeyCode::Down
|
||||
);
|
||||
if ctx.app.ui.minimap_until.is_some() && !(ctrl && is_arrow) {
|
||||
ctx.app.ui.minimap_until = None;
|
||||
}
|
||||
|
||||
if ctx.app.ui.show_title {
|
||||
ctx.app.ui.show_title = false;
|
||||
return InputResult::Continue;
|
||||
@@ -414,20 +424,25 @@ fn handle_normal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||
}
|
||||
|
||||
if ctrl {
|
||||
let minimap_timeout = Some(Instant::now() + Duration::from_millis(250));
|
||||
match key.code {
|
||||
KeyCode::Left => {
|
||||
ctx.app.ui.minimap_until = minimap_timeout;
|
||||
ctx.dispatch(AppCommand::PageLeft);
|
||||
return InputResult::Continue;
|
||||
}
|
||||
KeyCode::Right => {
|
||||
ctx.app.ui.minimap_until = minimap_timeout;
|
||||
ctx.dispatch(AppCommand::PageRight);
|
||||
return InputResult::Continue;
|
||||
}
|
||||
KeyCode::Up => {
|
||||
ctx.app.ui.minimap_until = minimap_timeout;
|
||||
ctx.dispatch(AppCommand::PageUp);
|
||||
return InputResult::Continue;
|
||||
}
|
||||
KeyCode::Down => {
|
||||
ctx.app.ui.minimap_until = minimap_timeout;
|
||||
ctx.dispatch(AppCommand::PageDown);
|
||||
return InputResult::Continue;
|
||||
}
|
||||
@@ -439,7 +454,8 @@ fn handle_normal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||
Page::Main => handle_main_page(ctx, key, ctrl),
|
||||
Page::Patterns => handle_patterns_page(ctx, key),
|
||||
Page::Audio => handle_audio_page(ctx, key),
|
||||
Page::Doc => handle_doc_page(ctx, key),
|
||||
Page::Help => handle_help_page(ctx, key),
|
||||
Page::Dict => handle_dict_page(ctx, key),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -865,16 +881,43 @@ fn handle_audio_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||
InputResult::Continue
|
||||
}
|
||||
|
||||
fn handle_doc_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||
fn handle_help_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||
match key.code {
|
||||
KeyCode::Char('j') | KeyCode::Down => ctx.dispatch(AppCommand::DocScrollDown(1)),
|
||||
KeyCode::Char('k') | KeyCode::Up => ctx.dispatch(AppCommand::DocScrollUp(1)),
|
||||
KeyCode::Char('h') | KeyCode::Left => ctx.dispatch(AppCommand::DocPrevCategory),
|
||||
KeyCode::Char('l') | KeyCode::Right => ctx.dispatch(AppCommand::DocNextCategory),
|
||||
KeyCode::Tab => ctx.dispatch(AppCommand::DocNextTopic),
|
||||
KeyCode::BackTab => ctx.dispatch(AppCommand::DocPrevTopic),
|
||||
KeyCode::PageDown => ctx.dispatch(AppCommand::DocScrollDown(10)),
|
||||
KeyCode::PageUp => ctx.dispatch(AppCommand::DocScrollUp(10)),
|
||||
KeyCode::Char('j') | KeyCode::Down => ctx.dispatch(AppCommand::HelpScrollDown(1)),
|
||||
KeyCode::Char('k') | KeyCode::Up => ctx.dispatch(AppCommand::HelpScrollUp(1)),
|
||||
KeyCode::Tab => ctx.dispatch(AppCommand::HelpNextTopic),
|
||||
KeyCode::BackTab => ctx.dispatch(AppCommand::HelpPrevTopic),
|
||||
KeyCode::PageDown => ctx.dispatch(AppCommand::HelpScrollDown(10)),
|
||||
KeyCode::PageUp => ctx.dispatch(AppCommand::HelpScrollUp(10)),
|
||||
KeyCode::Char('q') => {
|
||||
ctx.dispatch(AppCommand::OpenModal(Modal::ConfirmQuit {
|
||||
selected: false,
|
||||
}));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
InputResult::Continue
|
||||
}
|
||||
|
||||
fn handle_dict_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||
use crate::state::DictFocus;
|
||||
|
||||
match key.code {
|
||||
KeyCode::Tab => ctx.dispatch(AppCommand::DictToggleFocus),
|
||||
KeyCode::Char('j') | KeyCode::Down => {
|
||||
match ctx.app.ui.dict_focus {
|
||||
DictFocus::Categories => ctx.dispatch(AppCommand::DictNextCategory),
|
||||
DictFocus::Words => ctx.dispatch(AppCommand::DictScrollDown(1)),
|
||||
}
|
||||
}
|
||||
KeyCode::Char('k') | KeyCode::Up => {
|
||||
match ctx.app.ui.dict_focus {
|
||||
DictFocus::Categories => ctx.dispatch(AppCommand::DictPrevCategory),
|
||||
DictFocus::Words => ctx.dispatch(AppCommand::DictScrollUp(1)),
|
||||
}
|
||||
}
|
||||
KeyCode::PageDown => ctx.dispatch(AppCommand::DictScrollDown(10)),
|
||||
KeyCode::PageUp => ctx.dispatch(AppCommand::DictScrollUp(10)),
|
||||
KeyCode::Char('q') => {
|
||||
ctx.dispatch(AppCommand::OpenModal(Modal::ConfirmQuit {
|
||||
selected: false,
|
||||
|
||||
78
src/page.rs
78
src/page.rs
@@ -4,35 +4,87 @@ pub enum Page {
|
||||
Main,
|
||||
Patterns,
|
||||
Audio,
|
||||
Doc,
|
||||
Help,
|
||||
Dict,
|
||||
}
|
||||
|
||||
impl Page {
|
||||
/// All pages for iteration
|
||||
pub const ALL: &'static [Page] = &[
|
||||
Page::Main,
|
||||
Page::Patterns,
|
||||
Page::Audio,
|
||||
Page::Help,
|
||||
Page::Dict,
|
||||
];
|
||||
|
||||
/// Grid dimensions (cols, rows)
|
||||
pub const GRID_SIZE: (i8, i8) = (3, 2);
|
||||
|
||||
/// Grid position (col, row) for each page
|
||||
/// Layout:
|
||||
/// col 0 col 1 col 2
|
||||
/// row 0 Patterns Help
|
||||
/// row 1 Dict Sequencer Audio
|
||||
pub const fn grid_pos(self) -> (i8, i8) {
|
||||
match self {
|
||||
Page::Dict => (0, 1),
|
||||
Page::Main => (1, 1),
|
||||
Page::Patterns => (1, 0),
|
||||
Page::Audio => (2, 1),
|
||||
Page::Help => (2, 0),
|
||||
}
|
||||
}
|
||||
|
||||
/// Find page at grid position, if any
|
||||
pub fn at_pos(col: i8, row: i8) -> Option<Page> {
|
||||
Self::ALL.iter().copied().find(|p| p.grid_pos() == (col, row))
|
||||
}
|
||||
|
||||
/// Display name for the page
|
||||
pub const fn name(self) -> &'static str {
|
||||
match self {
|
||||
Page::Main => "Sequencer",
|
||||
Page::Patterns => "Patterns",
|
||||
Page::Audio => "Audio",
|
||||
Page::Help => "Help",
|
||||
Page::Dict => "Dict",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn left(&mut self) {
|
||||
*self = match self {
|
||||
Page::Main | Page::Patterns => Page::Doc,
|
||||
Page::Audio => Page::Main,
|
||||
Page::Doc => Page::Audio,
|
||||
let (col, row) = self.grid_pos();
|
||||
for offset in 1..=Self::GRID_SIZE.0 {
|
||||
let new_col = (col - offset).rem_euclid(Self::GRID_SIZE.0);
|
||||
if let Some(page) = Self::at_pos(new_col, row) {
|
||||
*self = page;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn right(&mut self) {
|
||||
*self = match self {
|
||||
Page::Main | Page::Patterns => Page::Audio,
|
||||
Page::Audio => Page::Doc,
|
||||
Page::Doc => Page::Main,
|
||||
let (col, row) = self.grid_pos();
|
||||
for offset in 1..=Self::GRID_SIZE.0 {
|
||||
let new_col = (col + offset).rem_euclid(Self::GRID_SIZE.0);
|
||||
if let Some(page) = Self::at_pos(new_col, row) {
|
||||
*self = page;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn up(&mut self) {
|
||||
if *self == Page::Main {
|
||||
*self = Page::Patterns;
|
||||
let (col, row) = self.grid_pos();
|
||||
if let Some(page) = Self::at_pos(col, row - 1) {
|
||||
*self = page;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn down(&mut self) {
|
||||
if *self == Page::Patterns {
|
||||
*self = Page::Main;
|
||||
let (col, row) = self.grid_pos();
|
||||
if let Some(page) = Self::at_pos(col, row + 1) {
|
||||
*self = page;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,4 +19,4 @@ pub use patterns_nav::{PatternsColumn, PatternsNav};
|
||||
pub use playback::PlaybackState;
|
||||
pub use project::ProjectState;
|
||||
pub use sample_browser::SampleBrowserState;
|
||||
pub use ui::UiState;
|
||||
pub use ui::{DictFocus, UiState};
|
||||
|
||||
@@ -11,18 +11,28 @@ pub struct Sparkle {
|
||||
pub life: u8,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum DictFocus {
|
||||
#[default]
|
||||
Categories,
|
||||
Words,
|
||||
}
|
||||
|
||||
pub struct UiState {
|
||||
pub sparkles: Vec<Sparkle>,
|
||||
pub status_message: Option<String>,
|
||||
pub flash_until: Option<Instant>,
|
||||
pub flash_color: Color,
|
||||
pub modal: Modal,
|
||||
pub doc_topic: usize,
|
||||
pub doc_scroll: usize,
|
||||
pub doc_category: usize,
|
||||
pub help_topic: usize,
|
||||
pub help_scroll: usize,
|
||||
pub dict_focus: DictFocus,
|
||||
pub dict_category: usize,
|
||||
pub dict_scroll: usize,
|
||||
pub show_title: bool,
|
||||
pub runtime_highlight: bool,
|
||||
pub show_completion: bool,
|
||||
pub minimap_until: Option<Instant>,
|
||||
}
|
||||
|
||||
impl Default for UiState {
|
||||
@@ -33,12 +43,15 @@ impl Default for UiState {
|
||||
flash_until: None,
|
||||
flash_color: Color::Green,
|
||||
modal: Modal::None,
|
||||
doc_topic: 0,
|
||||
doc_scroll: 0,
|
||||
doc_category: 0,
|
||||
help_topic: 0,
|
||||
help_scroll: 0,
|
||||
dict_focus: DictFocus::default(),
|
||||
dict_category: 0,
|
||||
dict_scroll: 0,
|
||||
show_title: true,
|
||||
runtime_highlight: false,
|
||||
show_completion: true,
|
||||
minimap_until: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
186
src/views/dict_view.rs
Normal file
186
src/views/dict_view.rs
Normal file
@@ -0,0 +1,186 @@
|
||||
use ratatui::layout::{Constraint, Layout, Rect};
|
||||
use ratatui::style::{Color, Modifier, Style};
|
||||
use ratatui::text::{Line as RLine, Span};
|
||||
use ratatui::widgets::{Block, Borders, List, ListItem, Paragraph};
|
||||
use ratatui::Frame;
|
||||
|
||||
use crate::app::App;
|
||||
use crate::model::{Word, WordCompile, WORDS};
|
||||
use crate::state::DictFocus;
|
||||
|
||||
const CATEGORIES: &[&str] = &[
|
||||
"Stack",
|
||||
"Arithmetic",
|
||||
"Comparison",
|
||||
"Logic",
|
||||
"Sound",
|
||||
"Variables",
|
||||
"Randomness",
|
||||
"Probability",
|
||||
"Context",
|
||||
"Music",
|
||||
"Time",
|
||||
"Parameters",
|
||||
];
|
||||
|
||||
pub fn render(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let [header_area, body_area] =
|
||||
Layout::vertical([Constraint::Length(5), Constraint::Fill(1)]).areas(area);
|
||||
|
||||
render_header(frame, header_area);
|
||||
|
||||
let [cat_area, words_area] =
|
||||
Layout::horizontal([Constraint::Length(16), Constraint::Fill(1)]).areas(body_area);
|
||||
|
||||
render_categories(frame, app, cat_area);
|
||||
render_words(frame, app, words_area);
|
||||
}
|
||||
|
||||
fn render_header(frame: &mut Frame, area: Rect) {
|
||||
use ratatui::widgets::Wrap;
|
||||
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(Color::Rgb(60, 60, 70)))
|
||||
.title("Dictionary");
|
||||
let para = Paragraph::new(desc)
|
||||
.style(Style::new().fg(Color::Rgb(140, 145, 155)))
|
||||
.wrap(Wrap { trim: false })
|
||||
.block(block);
|
||||
frame.render_widget(para, area);
|
||||
}
|
||||
|
||||
fn render_categories(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let focused = app.ui.dict_focus == DictFocus::Categories;
|
||||
|
||||
let items: Vec<ListItem> = CATEGORIES
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, name)| {
|
||||
let is_selected = i == app.ui.dict_category;
|
||||
let style = if is_selected && focused {
|
||||
Style::new().fg(Color::Yellow).add_modifier(Modifier::BOLD)
|
||||
} else if is_selected {
|
||||
Style::new().fg(Color::Cyan)
|
||||
} else {
|
||||
Style::new().fg(Color::White)
|
||||
};
|
||||
let prefix = if is_selected { "> " } else { " " };
|
||||
ListItem::new(format!("{prefix}{name}")).style(style)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let border_color = if focused { Color::Yellow } else { Color::Rgb(60, 60, 70) };
|
||||
let block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::new().fg(border_color))
|
||||
.title("Categories");
|
||||
let list = List::new(items).block(block);
|
||||
frame.render_widget(list, area);
|
||||
}
|
||||
|
||||
fn render_words(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let focused = app.ui.dict_focus == DictFocus::Words;
|
||||
let category = CATEGORIES[app.ui.dict_category];
|
||||
let words: Vec<&Word> = WORDS
|
||||
.iter()
|
||||
.filter(|w| word_category(w.name, &w.compile) == category)
|
||||
.collect();
|
||||
|
||||
let content_width = area.width.saturating_sub(2) as usize;
|
||||
|
||||
let mut lines: Vec<RLine> = Vec::new();
|
||||
|
||||
for word in &words {
|
||||
let name_bg = Color::Rgb(40, 50, 60);
|
||||
let name_style = Style::new()
|
||||
.fg(Color::Green)
|
||||
.bg(name_bg)
|
||||
.add_modifier(Modifier::BOLD);
|
||||
let name_line = format!(" {}", word.name);
|
||||
let padding = " ".repeat(content_width.saturating_sub(name_line.chars().count()));
|
||||
lines.push(RLine::from(Span::styled(
|
||||
format!("{name_line}{padding}"),
|
||||
name_style,
|
||||
)));
|
||||
|
||||
let stack_style = Style::new().fg(Color::Magenta);
|
||||
lines.push(RLine::from(vec![
|
||||
Span::raw(" "),
|
||||
Span::styled(word.stack.to_string(), stack_style),
|
||||
]));
|
||||
|
||||
let desc_style = Style::new().fg(Color::White);
|
||||
lines.push(RLine::from(vec![
|
||||
Span::raw(" "),
|
||||
Span::styled(word.desc.to_string(), desc_style),
|
||||
]));
|
||||
|
||||
let example_style = Style::new().fg(Color::Rgb(120, 130, 140));
|
||||
lines.push(RLine::from(vec![
|
||||
Span::raw(" "),
|
||||
Span::styled(format!("e.g. {}", word.example), example_style),
|
||||
]));
|
||||
|
||||
lines.push(RLine::from(""));
|
||||
}
|
||||
|
||||
let visible_height = 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 visible: Vec<RLine> = lines
|
||||
.into_iter()
|
||||
.skip(scroll)
|
||||
.take(visible_height)
|
||||
.collect();
|
||||
|
||||
let title = format!("{category} ({} words)", words.len());
|
||||
let border_color = if focused { Color::Yellow } else { Color::Rgb(60, 60, 70) };
|
||||
let block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::new().fg(border_color))
|
||||
.title(title);
|
||||
let para = Paragraph::new(visible).block(block);
|
||||
frame.render_widget(para, area);
|
||||
}
|
||||
|
||||
fn word_category(name: &str, compile: &WordCompile) -> &'static str {
|
||||
const STACK: &[&str] = &["dup", "drop", "swap", "over", "rot", "nip", "tuck"];
|
||||
const ARITH: &[&str] = &[
|
||||
"+", "-", "*", "/", "mod", "neg", "abs", "floor", "ceil", "round", "min", "max",
|
||||
];
|
||||
const CMP: &[&str] = &["=", "<>", "<", ">", "<=", ">="];
|
||||
const LOGIC: &[&str] = &["and", "or", "not"];
|
||||
const SOUND: &[&str] = &["sound", "s", "emit"];
|
||||
const VAR: &[&str] = &["get", "set"];
|
||||
const RAND: &[&str] = &["rand", "rrand", "seed", "coin", "chance", "choose", "cycle"];
|
||||
const MUSIC: &[&str] = &["mtof", "ftom"];
|
||||
const TIME: &[&str] = &[
|
||||
"at", "window", "pop", "div", "each", "tempo!", "[", "]", "?",
|
||||
];
|
||||
|
||||
match compile {
|
||||
WordCompile::Simple if STACK.contains(&name) => "Stack",
|
||||
WordCompile::Simple if ARITH.contains(&name) => "Arithmetic",
|
||||
WordCompile::Simple if CMP.contains(&name) => "Comparison",
|
||||
WordCompile::Simple if LOGIC.contains(&name) => "Logic",
|
||||
WordCompile::Simple if SOUND.contains(&name) => "Sound",
|
||||
WordCompile::Alias(_) => "Sound",
|
||||
WordCompile::Simple if VAR.contains(&name) => "Variables",
|
||||
WordCompile::Simple if RAND.contains(&name) => "Randomness",
|
||||
WordCompile::Probability(_) => "Probability",
|
||||
WordCompile::Context(_) => "Context",
|
||||
WordCompile::Simple if MUSIC.contains(&name) => "Music",
|
||||
WordCompile::Simple if TIME.contains(&name) => "Time",
|
||||
WordCompile::Param => "Parameters",
|
||||
_ => "Other",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn category_count() -> usize {
|
||||
CATEGORIES.len()
|
||||
}
|
||||
@@ -1,266 +0,0 @@
|
||||
use minimad::{Composite, CompositeStyle, Compound, Line};
|
||||
use ratatui::layout::{Constraint, Layout, Rect};
|
||||
use ratatui::style::{Color, Modifier, Style};
|
||||
use ratatui::text::{Line as RLine, Span};
|
||||
use ratatui::widgets::{Block, Borders, List, ListItem, Paragraph};
|
||||
use ratatui::Frame;
|
||||
|
||||
use crate::app::App;
|
||||
use crate::model::{Word, WordCompile, WORDS};
|
||||
|
||||
const STATIC_DOCS: &[(&str, &str)] = &[
|
||||
("Keybindings", include_str!("../../docs/keybindings.md")),
|
||||
("Sequencer", include_str!("../../docs/sequencer.md")),
|
||||
];
|
||||
|
||||
const TOPICS: &[&str] = &["Keybindings", "Forth Reference", "Sequencer"];
|
||||
|
||||
const CATEGORIES: &[&str] = &[
|
||||
"Stack",
|
||||
"Arithmetic",
|
||||
"Comparison",
|
||||
"Logic",
|
||||
"Sound",
|
||||
"Variables",
|
||||
"Randomness",
|
||||
"Probability",
|
||||
"Context",
|
||||
"Music",
|
||||
"Time",
|
||||
"Parameters",
|
||||
];
|
||||
|
||||
pub fn render(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let [topics_area, content_area] =
|
||||
Layout::horizontal([Constraint::Length(18), Constraint::Fill(1)]).areas(area);
|
||||
|
||||
render_topics(frame, app, topics_area);
|
||||
|
||||
let topic = TOPICS[app.ui.doc_topic];
|
||||
if topic == "Forth Reference" {
|
||||
render_forth_reference(frame, app, content_area);
|
||||
} else {
|
||||
render_markdown_content(frame, app, content_area, topic);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_topics(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let items: Vec<ListItem> = TOPICS
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, name)| {
|
||||
let style = if i == app.ui.doc_topic {
|
||||
Style::new().fg(Color::Cyan).add_modifier(Modifier::BOLD)
|
||||
} else {
|
||||
Style::new().fg(Color::White)
|
||||
};
|
||||
let prefix = if i == app.ui.doc_topic { "> " } else { " " };
|
||||
ListItem::new(format!("{prefix}{name}")).style(style)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let list = List::new(items).block(Block::default().borders(Borders::ALL).title("Topics"));
|
||||
frame.render_widget(list, area);
|
||||
}
|
||||
|
||||
fn render_markdown_content(frame: &mut Frame, app: &App, area: Rect, topic: &str) {
|
||||
let md = STATIC_DOCS
|
||||
.iter()
|
||||
.find(|(name, _)| *name == topic)
|
||||
.map(|(_, content)| *content)
|
||||
.unwrap_or("");
|
||||
let lines = parse_markdown(md);
|
||||
|
||||
let visible_height = 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.doc_scroll.min(max_scroll);
|
||||
|
||||
let visible: Vec<RLine> = lines
|
||||
.into_iter()
|
||||
.skip(scroll)
|
||||
.take(visible_height)
|
||||
.collect();
|
||||
|
||||
let para = Paragraph::new(visible).block(Block::default().borders(Borders::ALL).title(topic));
|
||||
frame.render_widget(para, area);
|
||||
}
|
||||
|
||||
fn render_forth_reference(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let [cat_area, words_area] =
|
||||
Layout::horizontal([Constraint::Length(14), Constraint::Fill(1)]).areas(area);
|
||||
|
||||
render_categories(frame, app, cat_area);
|
||||
render_words(frame, app, words_area);
|
||||
}
|
||||
|
||||
fn render_categories(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let items: Vec<ListItem> = CATEGORIES
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, name)| {
|
||||
let style = if i == app.ui.doc_category {
|
||||
Style::new().fg(Color::Yellow).add_modifier(Modifier::BOLD)
|
||||
} else {
|
||||
Style::new().fg(Color::White)
|
||||
};
|
||||
let prefix = if i == app.ui.doc_category { "> " } else { " " };
|
||||
ListItem::new(format!("{prefix}{name}")).style(style)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let list = List::new(items).block(Block::default().borders(Borders::ALL).title("Category"));
|
||||
frame.render_widget(list, area);
|
||||
}
|
||||
|
||||
fn render_words(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let category = CATEGORIES[app.ui.doc_category];
|
||||
let words: Vec<&Word> = WORDS
|
||||
.iter()
|
||||
.filter(|w| word_category(w.name, &w.compile) == category)
|
||||
.collect();
|
||||
|
||||
let word_style = Style::new().fg(Color::Green).add_modifier(Modifier::BOLD);
|
||||
let stack_style = Style::new().fg(Color::Magenta);
|
||||
let desc_style = Style::new().fg(Color::White);
|
||||
let example_style = Style::new().fg(Color::Rgb(150, 150, 150));
|
||||
|
||||
let mut lines: Vec<RLine> = Vec::new();
|
||||
|
||||
for word in &words {
|
||||
lines.push(RLine::from(vec![
|
||||
Span::styled(format!("{:<14}", word.name), word_style),
|
||||
Span::styled(format!("{:<18}", word.stack), stack_style),
|
||||
Span::styled(word.desc.to_string(), desc_style),
|
||||
]));
|
||||
lines.push(RLine::from(vec![
|
||||
Span::raw(" "),
|
||||
Span::styled(format!("e.g. {}", word.example), example_style),
|
||||
]));
|
||||
lines.push(RLine::from(""));
|
||||
}
|
||||
|
||||
let visible_height = 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.doc_scroll.min(max_scroll);
|
||||
|
||||
let visible: Vec<RLine> = lines
|
||||
.into_iter()
|
||||
.skip(scroll)
|
||||
.take(visible_height)
|
||||
.collect();
|
||||
|
||||
let title = format!("{category} ({} words)", words.len());
|
||||
let para = Paragraph::new(visible).block(Block::default().borders(Borders::ALL).title(title));
|
||||
frame.render_widget(para, area);
|
||||
}
|
||||
|
||||
fn word_category(name: &str, compile: &WordCompile) -> &'static str {
|
||||
const STACK: &[&str] = &["dup", "drop", "swap", "over", "rot", "nip", "tuck"];
|
||||
const ARITH: &[&str] = &[
|
||||
"+", "-", "*", "/", "mod", "neg", "abs", "floor", "ceil", "round", "min", "max",
|
||||
];
|
||||
const CMP: &[&str] = &["=", "<>", "<", ">", "<=", ">="];
|
||||
const LOGIC: &[&str] = &["and", "or", "not"];
|
||||
const SOUND: &[&str] = &["sound", "s", "emit"];
|
||||
const VAR: &[&str] = &["get", "set"];
|
||||
const RAND: &[&str] = &["rand", "rrand", "seed", "coin", "chance", "choose", "cycle"];
|
||||
const MUSIC: &[&str] = &["mtof", "ftom"];
|
||||
const TIME: &[&str] = &[
|
||||
"at", "window", "pop", "div", "each", "tempo!", "[", "]", "?",
|
||||
];
|
||||
|
||||
match compile {
|
||||
WordCompile::Simple if STACK.contains(&name) => "Stack",
|
||||
WordCompile::Simple if ARITH.contains(&name) => "Arithmetic",
|
||||
WordCompile::Simple if CMP.contains(&name) => "Comparison",
|
||||
WordCompile::Simple if LOGIC.contains(&name) => "Logic",
|
||||
WordCompile::Simple if SOUND.contains(&name) => "Sound",
|
||||
WordCompile::Alias(_) => "Sound",
|
||||
WordCompile::Simple if VAR.contains(&name) => "Variables",
|
||||
WordCompile::Simple if RAND.contains(&name) => "Randomness",
|
||||
WordCompile::Probability(_) => "Probability",
|
||||
WordCompile::Context(_) => "Context",
|
||||
WordCompile::Simple if MUSIC.contains(&name) => "Music",
|
||||
WordCompile::Simple if TIME.contains(&name) => "Time",
|
||||
WordCompile::Param => "Parameters",
|
||||
_ => "Other",
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_markdown(md: &str) -> Vec<RLine<'static>> {
|
||||
let text = minimad::Text::from(md);
|
||||
let mut lines = Vec::new();
|
||||
|
||||
for line in text.lines {
|
||||
match line {
|
||||
Line::Normal(composite) => {
|
||||
lines.push(composite_to_line(composite));
|
||||
}
|
||||
Line::TableRow(_) | Line::HorizontalRule | Line::CodeFence(_) | Line::TableRule(_) => {
|
||||
lines.push(RLine::from(""));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lines
|
||||
}
|
||||
|
||||
fn composite_to_line(composite: Composite) -> RLine<'static> {
|
||||
let base_style = match composite.style {
|
||||
CompositeStyle::Header(1) => Style::new()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD | Modifier::UNDERLINED),
|
||||
CompositeStyle::Header(2) => Style::new().fg(Color::Yellow).add_modifier(Modifier::BOLD),
|
||||
CompositeStyle::Header(_) => Style::new().fg(Color::Magenta).add_modifier(Modifier::BOLD),
|
||||
CompositeStyle::ListItem(_) => Style::new().fg(Color::White),
|
||||
CompositeStyle::Quote => Style::new().fg(Color::Rgb(150, 150, 150)),
|
||||
CompositeStyle::Code => Style::new().fg(Color::Green),
|
||||
CompositeStyle::Paragraph => Style::new().fg(Color::White),
|
||||
};
|
||||
|
||||
let prefix = match composite.style {
|
||||
CompositeStyle::ListItem(_) => " • ",
|
||||
CompositeStyle::Quote => " │ ",
|
||||
_ => "",
|
||||
};
|
||||
|
||||
let mut spans: Vec<Span<'static>> = Vec::new();
|
||||
if !prefix.is_empty() {
|
||||
spans.push(Span::styled(prefix.to_string(), base_style));
|
||||
}
|
||||
|
||||
for compound in composite.compounds {
|
||||
spans.push(compound_to_span(compound, base_style));
|
||||
}
|
||||
|
||||
RLine::from(spans)
|
||||
}
|
||||
|
||||
fn compound_to_span(compound: Compound, base: Style) -> Span<'static> {
|
||||
let mut style = base;
|
||||
|
||||
if compound.bold {
|
||||
style = style.add_modifier(Modifier::BOLD);
|
||||
}
|
||||
if compound.italic {
|
||||
style = style.add_modifier(Modifier::ITALIC);
|
||||
}
|
||||
if compound.code {
|
||||
style = Style::new().fg(Color::Green);
|
||||
}
|
||||
if compound.strikeout {
|
||||
style = style.add_modifier(Modifier::CROSSED_OUT);
|
||||
}
|
||||
|
||||
Span::styled(compound.src.to_string(), style)
|
||||
}
|
||||
|
||||
pub fn topic_count() -> usize {
|
||||
TOPICS.len()
|
||||
}
|
||||
|
||||
pub fn category_count() -> usize {
|
||||
CATEGORIES.len()
|
||||
}
|
||||
139
src/views/help_view.rs
Normal file
139
src/views/help_view.rs
Normal file
@@ -0,0 +1,139 @@
|
||||
use minimad::{Composite, CompositeStyle, Compound, Line};
|
||||
use ratatui::layout::{Constraint, Layout, Rect};
|
||||
use ratatui::style::{Color, Modifier, Style};
|
||||
use ratatui::text::{Line as RLine, Span};
|
||||
use ratatui::widgets::{Block, Borders, List, ListItem, Paragraph};
|
||||
use ratatui::Frame;
|
||||
|
||||
use crate::app::App;
|
||||
|
||||
const STATIC_DOCS: &[(&str, &str)] = &[
|
||||
("Keybindings", include_str!("../../docs/keybindings.md")),
|
||||
("Sequencer", include_str!("../../docs/sequencer.md")),
|
||||
];
|
||||
|
||||
const TOPICS: &[&str] = &["Keybindings", "Sequencer"];
|
||||
|
||||
pub fn render(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let [topics_area, content_area] =
|
||||
Layout::horizontal([Constraint::Length(18), Constraint::Fill(1)]).areas(area);
|
||||
|
||||
render_topics(frame, app, topics_area);
|
||||
|
||||
let topic = TOPICS[app.ui.help_topic];
|
||||
render_markdown_content(frame, app, content_area, topic);
|
||||
}
|
||||
|
||||
fn render_topics(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let items: Vec<ListItem> = TOPICS
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, name)| {
|
||||
let style = if i == app.ui.help_topic {
|
||||
Style::new().fg(Color::Cyan).add_modifier(Modifier::BOLD)
|
||||
} else {
|
||||
Style::new().fg(Color::White)
|
||||
};
|
||||
let prefix = if i == app.ui.help_topic { "> " } else { " " };
|
||||
ListItem::new(format!("{prefix}{name}")).style(style)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let list = List::new(items).block(Block::default().borders(Borders::ALL).title("Topics"));
|
||||
frame.render_widget(list, area);
|
||||
}
|
||||
|
||||
fn render_markdown_content(frame: &mut Frame, app: &App, area: Rect, topic: &str) {
|
||||
let md = STATIC_DOCS
|
||||
.iter()
|
||||
.find(|(name, _)| *name == topic)
|
||||
.map(|(_, content)| *content)
|
||||
.unwrap_or("");
|
||||
let lines = parse_markdown(md);
|
||||
|
||||
let visible_height = 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.help_scroll.min(max_scroll);
|
||||
|
||||
let visible: Vec<RLine> = lines
|
||||
.into_iter()
|
||||
.skip(scroll)
|
||||
.take(visible_height)
|
||||
.collect();
|
||||
|
||||
let para = Paragraph::new(visible).block(Block::default().borders(Borders::ALL).title(topic));
|
||||
frame.render_widget(para, area);
|
||||
}
|
||||
|
||||
fn parse_markdown(md: &str) -> Vec<RLine<'static>> {
|
||||
let text = minimad::Text::from(md);
|
||||
let mut lines = Vec::new();
|
||||
|
||||
for line in text.lines {
|
||||
match line {
|
||||
Line::Normal(composite) => {
|
||||
lines.push(composite_to_line(composite));
|
||||
}
|
||||
Line::TableRow(_) | Line::HorizontalRule | Line::CodeFence(_) | Line::TableRule(_) => {
|
||||
lines.push(RLine::from(""));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lines
|
||||
}
|
||||
|
||||
fn composite_to_line(composite: Composite) -> RLine<'static> {
|
||||
let base_style = match composite.style {
|
||||
CompositeStyle::Header(1) => Style::new()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD | Modifier::UNDERLINED),
|
||||
CompositeStyle::Header(2) => Style::new().fg(Color::Yellow).add_modifier(Modifier::BOLD),
|
||||
CompositeStyle::Header(_) => Style::new().fg(Color::Magenta).add_modifier(Modifier::BOLD),
|
||||
CompositeStyle::ListItem(_) => Style::new().fg(Color::White),
|
||||
CompositeStyle::Quote => Style::new().fg(Color::Rgb(150, 150, 150)),
|
||||
CompositeStyle::Code => Style::new().fg(Color::Green),
|
||||
CompositeStyle::Paragraph => Style::new().fg(Color::White),
|
||||
};
|
||||
|
||||
let prefix = match composite.style {
|
||||
CompositeStyle::ListItem(_) => " • ",
|
||||
CompositeStyle::Quote => " │ ",
|
||||
_ => "",
|
||||
};
|
||||
|
||||
let mut spans: Vec<Span<'static>> = Vec::new();
|
||||
if !prefix.is_empty() {
|
||||
spans.push(Span::styled(prefix.to_string(), base_style));
|
||||
}
|
||||
|
||||
for compound in composite.compounds {
|
||||
spans.push(compound_to_span(compound, base_style));
|
||||
}
|
||||
|
||||
RLine::from(spans)
|
||||
}
|
||||
|
||||
fn compound_to_span(compound: Compound, base: Style) -> Span<'static> {
|
||||
let mut style = base;
|
||||
|
||||
if compound.bold {
|
||||
style = style.add_modifier(Modifier::BOLD);
|
||||
}
|
||||
if compound.italic {
|
||||
style = style.add_modifier(Modifier::ITALIC);
|
||||
}
|
||||
if compound.code {
|
||||
style = Style::new().fg(Color::Green);
|
||||
}
|
||||
if compound.strikeout {
|
||||
style = style.add_modifier(Modifier::CROSSED_OUT);
|
||||
}
|
||||
|
||||
Span::styled(compound.src.to_string(), style)
|
||||
}
|
||||
|
||||
pub fn topic_count() -> usize {
|
||||
TOPICS.len()
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
pub mod audio_view;
|
||||
pub mod doc_view;
|
||||
pub mod dict_view;
|
||||
pub mod help_view;
|
||||
pub mod highlight;
|
||||
pub mod main_view;
|
||||
pub mod patterns_view;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use ratatui::layout::{Alignment, Constraint, Layout, Rect};
|
||||
use ratatui::style::{Color, Modifier, Style};
|
||||
use ratatui::text::{Line, Span};
|
||||
@@ -10,9 +12,9 @@ use crate::model::SourceSpan;
|
||||
use crate::page::Page;
|
||||
use crate::state::{Modal, PanelFocus, PatternField, SidePanel};
|
||||
use crate::views::highlight::{self, highlight_line, highlight_line_with_runtime};
|
||||
use crate::widgets::{ConfirmModal, ModalFrame, SampleBrowser, TextInputModal};
|
||||
use crate::widgets::{ConfirmModal, ModalFrame, NavMinimap, NavTile, SampleBrowser, TextInputModal};
|
||||
|
||||
use super::{audio_view, doc_view, main_view, patterns_view, title_view};
|
||||
use super::{audio_view, dict_view, help_view, main_view, patterns_view, title_view};
|
||||
|
||||
fn adjust_spans_for_line(spans: &[SourceSpan], line_start: usize, line_len: usize) -> Vec<SourceSpan> {
|
||||
spans.iter().filter_map(|s| {
|
||||
@@ -81,7 +83,8 @@ pub fn render(frame: &mut Frame, app: &mut App, link: &LinkState, snapshot: &Seq
|
||||
Page::Main => main_view::render(frame, app, snapshot, page_area),
|
||||
Page::Patterns => patterns_view::render(frame, app, snapshot, page_area),
|
||||
Page::Audio => audio_view::render(frame, app, link, page_area),
|
||||
Page::Doc => doc_view::render(frame, app, page_area),
|
||||
Page::Help => help_view::render(frame, app, page_area),
|
||||
Page::Dict => dict_view::render(frame, app, page_area),
|
||||
}
|
||||
|
||||
if let Some(side_area) = panel_area {
|
||||
@@ -90,6 +93,24 @@ pub fn render(frame: &mut Frame, app: &mut App, link: &LinkState, snapshot: &Seq
|
||||
|
||||
render_footer(frame, app, footer_area);
|
||||
render_modal(frame, app, snapshot, term);
|
||||
|
||||
let show_minimap = app
|
||||
.ui
|
||||
.minimap_until
|
||||
.map(|until| Instant::now() < until)
|
||||
.unwrap_or(false);
|
||||
|
||||
if show_minimap {
|
||||
let tiles: Vec<NavTile> = Page::ALL
|
||||
.iter()
|
||||
.map(|p| {
|
||||
let (col, row) = p.grid_pos();
|
||||
NavTile { col, row, name: p.name() }
|
||||
})
|
||||
.collect();
|
||||
let selected = app.page.grid_pos();
|
||||
NavMinimap::new(&tiles, selected).render_centered(frame, term);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_side_panel(frame: &mut Frame, app: &App, area: Rect) {
|
||||
@@ -241,7 +262,8 @@ fn render_footer(frame: &mut Frame, app: &App, area: Rect) {
|
||||
Page::Main => "[MAIN]",
|
||||
Page::Patterns => "[PATTERNS]",
|
||||
Page::Audio => "[AUDIO]",
|
||||
Page::Doc => "[DOC]",
|
||||
Page::Help => "[HELP]",
|
||||
Page::Dict => "[DICT]",
|
||||
};
|
||||
|
||||
let content = if let Some(ref msg) = app.ui.status_message {
|
||||
@@ -281,12 +303,16 @@ fn render_footer(frame: &mut Frame, app: &App, area: Rect) {
|
||||
("t", "Test"),
|
||||
("Space", "Play"),
|
||||
],
|
||||
Page::Doc => vec![
|
||||
Page::Help => vec![
|
||||
("↑↓", "Scroll"),
|
||||
("←→", "Category"),
|
||||
("Tab", "Topic"),
|
||||
("PgUp/Dn", "Page"),
|
||||
],
|
||||
Page::Dict => vec![
|
||||
("Tab", "Focus"),
|
||||
("↑↓", "Navigate"),
|
||||
("PgUp/Dn", "Page"),
|
||||
],
|
||||
};
|
||||
|
||||
let page_width = page_indicator.chars().count();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
pub use cagire_ratatui::{
|
||||
ConfirmModal, FileBrowserModal, ModalFrame, Orientation, SampleBrowser, Scope, Spectrum,
|
||||
TextInputModal, VuMeter,
|
||||
ConfirmModal, FileBrowserModal, ModalFrame, NavMinimap, NavTile, Orientation, SampleBrowser,
|
||||
Scope, Spectrum, TextInputModal, VuMeter,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user