Feat: continue refactoring
Some checks failed
Deploy Website / deploy (push) Failing after 4m48s

This commit is contained in:
2026-02-01 13:39:25 +01:00
parent dd853b8e1b
commit b47c789612
20 changed files with 766 additions and 581 deletions

View File

@@ -9,36 +9,52 @@ use crate::model::{Word, WORDS};
use crate::state::DictFocus;
use crate::theme;
const CATEGORIES: &[&str] = &[
enum CatEntry {
Section(&'static str),
Category(&'static str),
}
use CatEntry::{Category, Section};
const CATEGORIES: &[CatEntry] = &[
// Forth core
"Stack",
"Arithmetic",
"Comparison",
"Logic",
"Variables",
"Randomness",
"Probability",
"Lists",
"Definitions",
Section("Forth"),
Category("Stack"),
Category("Arithmetic"),
Category("Comparison"),
Category("Logic"),
Category("Control"),
Category("Variables"),
Category("Probability"),
Category("Definitions"),
// Live coding
"Sound",
"Time",
"Context",
"Music",
"LFO",
Section("Live Coding"),
Category("Sound"),
Category("Time"),
Category("Context"),
Category("Music"),
Category("LFO"),
// Synthesis
"Oscillator",
"Envelope",
"Pitch Env",
"Gain",
"Sample",
Section("Synthesis"),
Category("Oscillator"),
Category("Wavetable"),
Category("Generator"),
Category("Envelope"),
Category("Sample"),
// Effects
"Filter",
"Modulation",
"Mod FX",
"Lo-fi",
"Delay",
"Reverb",
Section("Effects"),
Category("Filter"),
Category("FM"),
Category("Modulation"),
Category("Mod FX"),
Category("Lo-fi"),
Category("Stereo"),
Category("Delay"),
Category("Reverb"),
// External I/O
Section("I/O"),
Category("MIDI"),
Category("Desktop"),
];
pub fn render(frame: &mut Frame, app: &App, area: Rect) {
@@ -76,22 +92,67 @@ 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 visible_height = area.height.saturating_sub(2) as usize;
let total_items = CATEGORIES.len();
// Find the visual index of the selected category (including sections)
let selected_visual_idx = {
let mut visual = 0;
let mut cat_count = 0;
for entry in CATEGORIES.iter() {
if let Category(_) = entry {
if cat_count == app.ui.dict_category {
break;
}
cat_count += 1;
}
visual += 1;
}
visual
};
// Calculate scroll to keep selection visible (centered when possible)
let scroll = if selected_visual_idx < visible_height / 2 {
0
} else if selected_visual_idx > total_items.saturating_sub(visible_height / 2) {
total_items.saturating_sub(visible_height)
} else {
selected_visual_idx.saturating_sub(visible_height / 2)
};
// Count categories before the scroll offset to track cat_idx correctly
let mut cat_idx = CATEGORIES
.iter()
.take(scroll)
.filter(|e| matches!(e, Category(_)))
.count();
let items: Vec<ListItem> = CATEGORIES
.iter()
.enumerate()
.map(|(i, name)| {
let is_selected = i == app.ui.dict_category;
let style = if dimmed {
Style::new().fg(theme.dict.category_dimmed)
} else if is_selected && focused {
Style::new().fg(theme.dict.category_focused).add_modifier(Modifier::BOLD)
} else if is_selected {
Style::new().fg(theme.dict.category_selected)
} else {
Style::new().fg(theme.dict.category_normal)
};
let prefix = if is_selected && !dimmed { "> " } else { " " };
ListItem::new(format!("{prefix}{name}")).style(style)
.skip(scroll)
.take(visible_height)
.map(|entry| match entry {
Section(name) => {
let style = Style::new().fg(theme.ui.text_dim);
ListItem::new(format!("{name}")).style(style)
}
Category(name) => {
let is_selected = cat_idx == app.ui.dict_category;
let style = if dimmed {
Style::new().fg(theme.dict.category_dimmed)
} else if is_selected && focused {
Style::new()
.fg(theme.dict.category_focused)
.add_modifier(Modifier::BOLD)
} else if is_selected {
Style::new().fg(theme.dict.category_selected)
} else {
Style::new().fg(theme.dict.category_normal)
};
let prefix = if is_selected && !dimmed { "> " } else { " " };
cat_idx += 1;
ListItem::new(format!("{prefix}{name}")).style(style)
}
})
.collect();
@@ -104,6 +165,17 @@ fn render_categories(frame: &mut Frame, app: &App, area: Rect, dimmed: bool) {
frame.render_widget(list, area);
}
fn get_category_name(index: usize) -> &'static str {
CATEGORIES
.iter()
.filter_map(|e| match e {
Category(name) => Some(*name),
Section(_) => None,
})
.nth(index)
.unwrap_or("Unknown")
}
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;
@@ -119,7 +191,7 @@ fn render_words(frame: &mut Frame, app: &App, area: Rect, is_searching: bool) {
})
.collect()
} else {
let category = CATEGORIES[app.ui.dict_category];
let category = get_category_name(app.ui.dict_category);
WORDS
.iter()
.filter(|w| w.category == category)
@@ -195,18 +267,12 @@ fn render_words(frame: &mut Frame, app: &App, area: Rect, is_searching: bool) {
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 visible: Vec<RLine> = lines
.into_iter()
.skip(scroll)
.take(visible_height)
.collect();
let scroll = app.ui.dict_scroll().min(max_scroll);
let title = if is_searching {
format!("Search: {} matches", words.len())
} else {
let category = CATEGORIES[app.ui.dict_category];
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 };
@@ -214,7 +280,9 @@ fn render_words(frame: &mut Frame, app: &App, area: Rect, is_searching: bool) {
.borders(Borders::ALL)
.border_style(Style::new().fg(border_color))
.title(title);
let para = Paragraph::new(visible).block(block);
let para = Paragraph::new(lines)
.scroll((scroll as u16, 0))
.block(block);
frame.render_widget(para, content_area);
}
@@ -232,5 +300,8 @@ fn render_search_bar(frame: &mut Frame, app: &App, area: Rect) {
}
pub fn category_count() -> usize {
CATEGORIES.len()
CATEGORIES
.iter()
.filter(|e| matches!(e, Category(_)))
.count()
}