Fixing color schemes

This commit is contained in:
2026-01-30 20:15:43 +01:00
parent 2731eea037
commit 584dbb6aad
22 changed files with 2888 additions and 482 deletions

View File

@@ -1,4 +1,4 @@
use crate::theme::confirm; use crate::theme;
use ratatui::layout::{Alignment, Constraint, Layout, Rect}; use ratatui::layout::{Alignment, Constraint, Layout, Rect};
use ratatui::style::Style; use ratatui::style::Style;
use ratatui::text::{Line, Span}; use ratatui::text::{Line, Span};
@@ -23,10 +23,11 @@ impl<'a> ConfirmModal<'a> {
} }
pub fn render_centered(self, frame: &mut Frame, term: Rect) { pub fn render_centered(self, frame: &mut Frame, term: Rect) {
let t = theme::get();
let inner = ModalFrame::new(self.title) let inner = ModalFrame::new(self.title)
.width(30) .width(30)
.height(5) .height(5)
.border_color(confirm::BORDER) .border_color(t.confirm.border)
.render_centered(frame, term); .render_centered(frame, term);
let rows = Layout::vertical([Constraint::Length(1), Constraint::Length(1)]).split(inner); let rows = Layout::vertical([Constraint::Length(1), Constraint::Length(1)]).split(inner);
@@ -37,12 +38,12 @@ impl<'a> ConfirmModal<'a> {
); );
let yes_style = if self.selected { let yes_style = if self.selected {
Style::new().fg(confirm::BUTTON_SELECTED_FG).bg(confirm::BUTTON_SELECTED_BG) Style::new().fg(t.confirm.button_selected_fg).bg(t.confirm.button_selected_bg)
} else { } else {
Style::default() Style::default()
}; };
let no_style = if !self.selected { let no_style = if !self.selected {
Style::new().fg(confirm::BUTTON_SELECTED_FG).bg(confirm::BUTTON_SELECTED_BG) Style::new().fg(t.confirm.button_selected_fg).bg(t.confirm.button_selected_bg)
} else { } else {
Style::default() Style::default()
}; };

View File

@@ -1,4 +1,4 @@
use crate::theme::editor_widget; use crate::theme;
use ratatui::{ use ratatui::{
layout::Rect, layout::Rect,
style::{Modifier, Style}, style::{Modifier, Style},
@@ -333,9 +333,10 @@ impl Editor {
} }
pub fn render(&self, frame: &mut Frame, area: Rect, highlighter: Highlighter) { pub fn render(&self, frame: &mut Frame, area: Rect, highlighter: Highlighter) {
let t = theme::get();
let (cursor_row, cursor_col) = self.text.cursor(); let (cursor_row, cursor_col) = self.text.cursor();
let cursor_style = Style::default().bg(editor_widget::CURSOR_BG).fg(editor_widget::CURSOR_FG); let cursor_style = Style::default().bg(t.editor_widget.cursor_bg).fg(t.editor_widget.cursor_fg);
let selection_style = Style::default().bg(editor_widget::SELECTION_BG); let selection_style = Style::default().bg(t.editor_widget.selection_bg);
let selection = self.text.selection_range(); let selection = self.text.selection_range();
@@ -383,6 +384,7 @@ impl Editor {
} }
fn render_completion(&self, frame: &mut Frame, editor_area: Rect, cursor_row: usize) { fn render_completion(&self, frame: &mut Frame, editor_area: Rect, cursor_row: usize) {
let t = theme::get();
let max_visible: usize = 6; let max_visible: usize = 6;
let list_width: u16 = 18; let list_width: u16 = 18;
let doc_width: u16 = 40; let doc_width: u16 = 40;
@@ -413,9 +415,9 @@ impl Editor {
let list_area = Rect::new(popup_x, popup_y, list_width, total_height); let list_area = Rect::new(popup_x, popup_y, list_width, total_height);
frame.render_widget(Clear, list_area); frame.render_widget(Clear, list_area);
let highlight_style = Style::default().fg(editor_widget::COMPLETION_SELECTED).add_modifier(Modifier::BOLD); let highlight_style = Style::default().fg(t.editor_widget.completion_selected).add_modifier(Modifier::BOLD);
let normal_style = Style::default().fg(editor_widget::COMPLETION_FG); let normal_style = Style::default().fg(t.editor_widget.completion_fg);
let bg_style = Style::default().bg(editor_widget::COMPLETION_BG); let bg_style = Style::default().bg(t.editor_widget.completion_bg);
let list_lines: Vec<Line> = (scroll_offset..scroll_offset + visible_count) let list_lines: Vec<Line> = (scroll_offset..scroll_offset + visible_count)
.map(|i| { .map(|i| {
@@ -428,7 +430,7 @@ impl Editor {
}; };
let prefix = if i == self.completion.cursor { "> " } else { " " }; let prefix = if i == self.completion.cursor { "> " } else { " " };
let display = format!("{prefix}{name:<width$}", width = list_width as usize - 2); let display = format!("{prefix}{name:<width$}", width = list_width as usize - 2);
Line::from(Span::styled(display, style.bg(editor_widget::COMPLETION_BG))) Line::from(Span::styled(display, style.bg(t.editor_widget.completion_bg)))
}) })
.collect(); .collect();
@@ -451,15 +453,15 @@ impl Editor {
let candidate = &self.completion.candidates[selected_idx]; let candidate = &self.completion.candidates[selected_idx];
let name_style = Style::default() let name_style = Style::default()
.fg(editor_widget::COMPLETION_SELECTED) .fg(t.editor_widget.completion_selected)
.add_modifier(Modifier::BOLD) .add_modifier(Modifier::BOLD)
.bg(editor_widget::COMPLETION_BG); .bg(t.editor_widget.completion_bg);
let desc_style = Style::default() let desc_style = Style::default()
.fg(editor_widget::COMPLETION_FG) .fg(t.editor_widget.completion_fg)
.bg(editor_widget::COMPLETION_BG); .bg(t.editor_widget.completion_bg);
let example_style = Style::default() let example_style = Style::default()
.fg(editor_widget::COMPLETION_EXAMPLE) .fg(t.editor_widget.completion_example)
.bg(editor_widget::COMPLETION_BG); .bg(t.editor_widget.completion_bg);
let w = doc_width as usize; let w = doc_width as usize;
let mut doc_lines: Vec<Line> = Vec::new(); let mut doc_lines: Vec<Line> = Vec::new();

View File

@@ -1,4 +1,4 @@
use crate::theme::ui; use crate::theme;
use ratatui::layout::Rect; use ratatui::layout::Rect;
use ratatui::style::{Color, Style}; use ratatui::style::{Color, Style};
use ratatui::widgets::{Block, Borders, Clear, Paragraph}; use ratatui::widgets::{Block, Borders, Clear, Paragraph};
@@ -8,7 +8,7 @@ pub struct ModalFrame<'a> {
title: &'a str, title: &'a str,
width: u16, width: u16,
height: u16, height: u16,
border_color: Color, border_color: Option<Color>,
} }
impl<'a> ModalFrame<'a> { impl<'a> ModalFrame<'a> {
@@ -17,7 +17,7 @@ impl<'a> ModalFrame<'a> {
title, title,
width: 40, width: 40,
height: 5, height: 5,
border_color: ui::TEXT_PRIMARY, border_color: None,
} }
} }
@@ -32,11 +32,12 @@ impl<'a> ModalFrame<'a> {
} }
pub fn border_color(mut self, c: Color) -> Self { pub fn border_color(mut self, c: Color) -> Self {
self.border_color = c; self.border_color = Some(c);
self self
} }
pub fn render_centered(&self, frame: &mut Frame, term: Rect) -> Rect { pub fn render_centered(&self, frame: &mut Frame, term: Rect) -> Rect {
let t = theme::get();
let width = self.width.min(term.width.saturating_sub(4)); let width = self.width.min(term.width.saturating_sub(4));
let height = self.height.min(term.height.saturating_sub(4)); let height = self.height.min(term.height.saturating_sub(4));
@@ -51,15 +52,16 @@ impl<'a> ModalFrame<'a> {
for row in 0..area.height { for row in 0..area.height {
let line_area = Rect::new(area.x, area.y + row, area.width, 1); let line_area = Rect::new(area.x, area.y + row, area.width, 1);
frame.render_widget( frame.render_widget(
Paragraph::new(bg_fill.clone()).style(Style::new().bg(ui::BG)), Paragraph::new(bg_fill.clone()).style(Style::new().bg(t.ui.bg)),
line_area, line_area,
); );
} }
let border_color = self.border_color.unwrap_or(t.ui.text_primary);
let block = Block::default() let block = Block::default()
.borders(Borders::ALL) .borders(Borders::ALL)
.title(self.title) .title(self.title)
.border_style(Style::new().fg(self.border_color)); .border_style(Style::new().fg(border_color));
let inner = block.inner(area); let inner = block.inner(area);
frame.render_widget(block, area); frame.render_widget(block, area);

View File

@@ -1,4 +1,4 @@
use crate::theme::{nav, ui}; use crate::theme;
use ratatui::layout::{Alignment, Rect}; use ratatui::layout::{Alignment, Rect};
use ratatui::style::Style; use ratatui::style::Style;
use ratatui::widgets::{Clear, Paragraph}; use ratatui::widgets::{Clear, Paragraph};
@@ -50,11 +50,12 @@ impl<'a> NavMinimap<'a> {
frame.render_widget(Clear, area); frame.render_widget(Clear, area);
// Fill background with theme color // Fill background with theme color
let t = theme::get();
let bg_fill = " ".repeat(area.width as usize); let bg_fill = " ".repeat(area.width as usize);
for row in 0..area.height { for row in 0..area.height {
let line_area = Rect::new(area.x, area.y + row, area.width, 1); let line_area = Rect::new(area.x, area.y + row, area.width, 1);
frame.render_widget( frame.render_widget(
Paragraph::new(bg_fill.clone()).style(Style::new().bg(ui::BG)), Paragraph::new(bg_fill.clone()).style(Style::new().bg(t.ui.bg)),
line_area, line_area,
); );
} }
@@ -72,10 +73,11 @@ impl<'a> NavMinimap<'a> {
} }
fn render_tile(&self, frame: &mut Frame, area: Rect, label: &str, is_selected: bool) { fn render_tile(&self, frame: &mut Frame, area: Rect, label: &str, is_selected: bool) {
let t = theme::get();
let (bg, fg) = if is_selected { let (bg, fg) = if is_selected {
(nav::SELECTED_BG, nav::SELECTED_FG) (t.nav.selected_bg, t.nav.selected_fg)
} else { } else {
(nav::UNSELECTED_BG, nav::UNSELECTED_FG) (t.nav.unselected_bg, t.nav.unselected_fg)
}; };
// Fill background // Fill background

File diff suppressed because it is too large Load Diff

View File

@@ -100,6 +100,7 @@ impl App {
show_spectrum: self.audio.config.show_spectrum, show_spectrum: self.audio.config.show_spectrum,
show_completion: self.ui.show_completion, show_completion: self.ui.show_completion,
flash_brightness: self.ui.flash_brightness, flash_brightness: self.ui.flash_brightness,
color_scheme: self.ui.color_scheme,
..Default::default() ..Default::default()
}, },
link: crate::settings::LinkSettings { link: crate::settings::LinkSettings {

View File

@@ -1217,6 +1217,15 @@ fn handle_options_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
KeyCode::Up | KeyCode::BackTab => ctx.app.options.prev_focus(), KeyCode::Up | KeyCode::BackTab => ctx.app.options.prev_focus(),
KeyCode::Left | KeyCode::Right => { KeyCode::Left | KeyCode::Right => {
match ctx.app.options.focus { match ctx.app.options.focus {
OptionsFocus::ColorScheme => {
let new_scheme = if key.code == KeyCode::Left {
ctx.app.ui.color_scheme.prev()
} else {
ctx.app.ui.color_scheme.next()
};
ctx.app.ui.color_scheme = new_scheme;
crate::theme::set(new_scheme.to_theme());
}
OptionsFocus::RefreshRate => ctx.app.audio.toggle_refresh_rate(), OptionsFocus::RefreshRate => ctx.app.audio.toggle_refresh_rate(),
OptionsFocus::RuntimeHighlight => { OptionsFocus::RuntimeHighlight => {
ctx.app.ui.runtime_highlight = !ctx.app.ui.runtime_highlight ctx.app.ui.runtime_highlight = !ctx.app.ui.runtime_highlight

View File

@@ -98,6 +98,8 @@ fn main() -> io::Result<()> {
app.audio.config.show_spectrum = settings.display.show_spectrum; app.audio.config.show_spectrum = settings.display.show_spectrum;
app.ui.show_completion = settings.display.show_completion; app.ui.show_completion = settings.display.show_completion;
app.ui.flash_brightness = settings.display.flash_brightness; app.ui.flash_brightness = settings.display.flash_brightness;
app.ui.color_scheme = settings.display.color_scheme;
theme::set(settings.display.color_scheme.to_theme());
let metrics = Arc::new(EngineMetrics::default()); let metrics = Arc::new(EngineMetrics::default());
let scope_buffer = Arc::new(ScopeBuffer::new()); let scope_buffer = Arc::new(ScopeBuffer::new());

View File

@@ -1,5 +1,7 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::state::ColorScheme;
const APP_NAME: &str = "cagire"; const APP_NAME: &str = "cagire";
#[derive(Debug, Default, Serialize, Deserialize)] #[derive(Debug, Default, Serialize, Deserialize)]
@@ -36,6 +38,8 @@ pub struct DisplaySettings {
pub flash_brightness: f32, pub flash_brightness: f32,
#[serde(default = "default_font")] #[serde(default = "default_font")]
pub font: String, pub font: String,
#[serde(default)]
pub color_scheme: ColorScheme,
} }
fn default_font() -> String { fn default_font() -> String {
@@ -76,6 +80,7 @@ impl Default for DisplaySettings {
show_completion: true, show_completion: true,
flash_brightness: 1.0, flash_brightness: 1.0,
font: default_font(), font: default_font(),
color_scheme: ColorScheme::default(),
} }
} }
} }

65
src/state/color_scheme.rs Normal file
View File

@@ -0,0 +1,65 @@
use serde::{Deserialize, Serialize};
use crate::theme::ThemeColors;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum ColorScheme {
#[default]
CatppuccinMocha,
CatppuccinLatte,
Nord,
Dracula,
GruvboxDark,
Monokai,
PitchBlack,
}
impl ColorScheme {
pub fn label(self) -> &'static str {
match self {
Self::CatppuccinMocha => "Catppuccin Mocha",
Self::CatppuccinLatte => "Catppuccin Latte",
Self::Nord => "Nord",
Self::Dracula => "Dracula",
Self::GruvboxDark => "Gruvbox Dark",
Self::Monokai => "Monokai",
Self::PitchBlack => "Pitch Black",
}
}
pub fn next(self) -> Self {
match self {
Self::CatppuccinMocha => Self::CatppuccinLatte,
Self::CatppuccinLatte => Self::Nord,
Self::Nord => Self::Dracula,
Self::Dracula => Self::GruvboxDark,
Self::GruvboxDark => Self::Monokai,
Self::Monokai => Self::PitchBlack,
Self::PitchBlack => Self::CatppuccinMocha,
}
}
pub fn prev(self) -> Self {
match self {
Self::CatppuccinMocha => Self::PitchBlack,
Self::CatppuccinLatte => Self::CatppuccinMocha,
Self::Nord => Self::CatppuccinLatte,
Self::Dracula => Self::Nord,
Self::GruvboxDark => Self::Dracula,
Self::Monokai => Self::GruvboxDark,
Self::PitchBlack => Self::Monokai,
}
}
pub fn to_theme(self) -> ThemeColors {
match self {
Self::CatppuccinMocha => ThemeColors::catppuccin_mocha(),
Self::CatppuccinLatte => ThemeColors::catppuccin_latte(),
Self::Nord => ThemeColors::nord(),
Self::Dracula => ThemeColors::dracula(),
Self::GruvboxDark => ThemeColors::gruvbox_dark(),
Self::Monokai => ThemeColors::monokai(),
Self::PitchBlack => ThemeColors::pitch_black(),
}
}
}

View File

@@ -1,4 +1,5 @@
pub mod audio; pub mod audio;
pub mod color_scheme;
pub mod editor; pub mod editor;
pub mod file_browser; pub mod file_browser;
pub mod live_keys; pub mod live_keys;
@@ -12,6 +13,7 @@ pub mod sample_browser;
pub mod ui; pub mod ui;
pub use audio::{AudioSettings, DeviceKind, EngineSection, Metrics, SettingKind}; pub use audio::{AudioSettings, DeviceKind, EngineSection, Metrics, SettingKind};
pub use color_scheme::ColorScheme;
pub use options::{OptionsFocus, OptionsState}; pub use options::{OptionsFocus, OptionsState};
pub use editor::{CopiedStepData, CopiedSteps, EditorContext, Focus, PatternField, PatternPropsField, StackCache}; pub use editor::{CopiedStepData, CopiedSteps, EditorContext, Focus, PatternField, PatternPropsField, StackCache};
pub use live_keys::LiveKeyState; pub use live_keys::LiveKeyState;

View File

@@ -1,6 +1,7 @@
#[derive(Clone, Copy, PartialEq, Eq, Default)] #[derive(Clone, Copy, PartialEq, Eq, Default)]
pub enum OptionsFocus { pub enum OptionsFocus {
#[default] #[default]
ColorScheme,
RefreshRate, RefreshRate,
RuntimeHighlight, RuntimeHighlight,
ShowScope, ShowScope,
@@ -20,6 +21,7 @@ pub struct OptionsState {
impl OptionsState { impl OptionsState {
pub fn next_focus(&mut self) { pub fn next_focus(&mut self) {
self.focus = match self.focus { self.focus = match self.focus {
OptionsFocus::ColorScheme => OptionsFocus::RefreshRate,
OptionsFocus::RefreshRate => OptionsFocus::RuntimeHighlight, OptionsFocus::RefreshRate => OptionsFocus::RuntimeHighlight,
OptionsFocus::RuntimeHighlight => OptionsFocus::ShowScope, OptionsFocus::RuntimeHighlight => OptionsFocus::ShowScope,
OptionsFocus::ShowScope => OptionsFocus::ShowSpectrum, OptionsFocus::ShowScope => OptionsFocus::ShowSpectrum,
@@ -28,13 +30,14 @@ impl OptionsState {
OptionsFocus::FlashBrightness => OptionsFocus::LinkEnabled, OptionsFocus::FlashBrightness => OptionsFocus::LinkEnabled,
OptionsFocus::LinkEnabled => OptionsFocus::StartStopSync, OptionsFocus::LinkEnabled => OptionsFocus::StartStopSync,
OptionsFocus::StartStopSync => OptionsFocus::Quantum, OptionsFocus::StartStopSync => OptionsFocus::Quantum,
OptionsFocus::Quantum => OptionsFocus::RefreshRate, OptionsFocus::Quantum => OptionsFocus::ColorScheme,
}; };
} }
pub fn prev_focus(&mut self) { pub fn prev_focus(&mut self) {
self.focus = match self.focus { self.focus = match self.focus {
OptionsFocus::RefreshRate => OptionsFocus::Quantum, OptionsFocus::ColorScheme => OptionsFocus::Quantum,
OptionsFocus::RefreshRate => OptionsFocus::ColorScheme,
OptionsFocus::RuntimeHighlight => OptionsFocus::RefreshRate, OptionsFocus::RuntimeHighlight => OptionsFocus::RefreshRate,
OptionsFocus::ShowScope => OptionsFocus::RuntimeHighlight, OptionsFocus::ShowScope => OptionsFocus::RuntimeHighlight,
OptionsFocus::ShowSpectrum => OptionsFocus::ShowScope, OptionsFocus::ShowSpectrum => OptionsFocus::ShowScope,

View File

@@ -2,7 +2,7 @@ use std::time::{Duration, Instant};
use cagire_ratatui::Sparkles; use cagire_ratatui::Sparkles;
use crate::state::Modal; use crate::state::{ColorScheme, Modal};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum FlashKind { pub enum FlashKind {
@@ -41,6 +41,7 @@ pub struct UiState {
pub last_event_count: usize, pub last_event_count: usize,
pub event_flash: f32, pub event_flash: f32,
pub flash_brightness: f32, pub flash_brightness: f32,
pub color_scheme: ColorScheme,
} }
impl Default for UiState { impl Default for UiState {
@@ -67,6 +68,7 @@ impl Default for UiState {
last_event_count: 0, last_event_count: 0,
event_flash: 0.0, event_flash: 0.0,
flash_brightness: 1.0, flash_brightness: 1.0,
color_scheme: ColorScheme::default(),
} }
} }
} }

View File

@@ -7,7 +7,7 @@ use ratatui::Frame;
use crate::app::App; use crate::app::App;
use crate::model::{Word, WORDS}; use crate::model::{Word, WORDS};
use crate::state::DictFocus; use crate::state::DictFocus;
use crate::theme::{dict, search}; use crate::theme;
const CATEGORIES: &[&str] = &[ const CATEGORIES: &[&str] = &[
// Forth core // Forth core
@@ -57,21 +57,23 @@ pub fn render(frame: &mut Frame, app: &App, area: Rect) {
fn render_header(frame: &mut Frame, area: Rect) { fn render_header(frame: &mut Frame, area: Rect) {
use ratatui::widgets::Wrap; use ratatui::widgets::Wrap;
let theme = theme::get();
let desc = "Forth uses a stack: values are pushed, functions (called words) consume and \ 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, \ 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 )."; pushes 7. This page lists all words with their signature ( inputs -- outputs ).";
let block = Block::default() let block = Block::default()
.borders(Borders::ALL) .borders(Borders::ALL)
.border_style(Style::new().fg(dict::BORDER_NORMAL)) .border_style(Style::new().fg(theme.dict.border_normal))
.title("Dictionary"); .title("Dictionary");
let para = Paragraph::new(desc) let para = Paragraph::new(desc)
.style(Style::new().fg(dict::HEADER_DESC)) .style(Style::new().fg(theme.dict.header_desc))
.wrap(Wrap { trim: false }) .wrap(Wrap { trim: false })
.block(block); .block(block);
frame.render_widget(para, area); frame.render_widget(para, area);
} }
fn render_categories(frame: &mut Frame, app: &App, area: Rect, dimmed: bool) { 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 focused = app.ui.dict_focus == DictFocus::Categories && !dimmed;
let items: Vec<ListItem> = CATEGORIES let items: Vec<ListItem> = CATEGORIES
@@ -80,20 +82,20 @@ fn render_categories(frame: &mut Frame, app: &App, area: Rect, dimmed: bool) {
.map(|(i, name)| { .map(|(i, name)| {
let is_selected = i == app.ui.dict_category; let is_selected = i == app.ui.dict_category;
let style = if dimmed { let style = if dimmed {
Style::new().fg(dict::CATEGORY_DIMMED) Style::new().fg(theme.dict.category_dimmed)
} else if is_selected && focused { } else if is_selected && focused {
Style::new().fg(dict::CATEGORY_FOCUSED).add_modifier(Modifier::BOLD) Style::new().fg(theme.dict.category_focused).add_modifier(Modifier::BOLD)
} else if is_selected { } else if is_selected {
Style::new().fg(dict::CATEGORY_SELECTED) Style::new().fg(theme.dict.category_selected)
} else { } else {
Style::new().fg(dict::CATEGORY_NORMAL) Style::new().fg(theme.dict.category_normal)
}; };
let prefix = if is_selected && !dimmed { "> " } else { " " }; let prefix = if is_selected && !dimmed { "> " } else { " " };
ListItem::new(format!("{prefix}{name}")).style(style) ListItem::new(format!("{prefix}{name}")).style(style)
}) })
.collect(); .collect();
let border_color = if focused { dict::BORDER_FOCUSED } else { 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))
@@ -103,6 +105,7 @@ fn render_categories(frame: &mut Frame, app: &App, area: Rect, dimmed: bool) {
} }
fn render_words(frame: &mut Frame, app: &App, area: Rect, is_searching: bool) { 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 focused = app.ui.dict_focus == DictFocus::Words;
// Filter words by search query or category // Filter words by search query or category
@@ -143,12 +146,12 @@ fn render_words(frame: &mut Frame, app: &App, area: Rect, is_searching: bool) {
let mut lines: Vec<RLine> = Vec::new(); let mut lines: Vec<RLine> = Vec::new();
for word in &words { for word in &words {
let name_bg = dict::WORD_BG; let name_bg = theme.dict.word_bg;
let name_style = Style::new() let name_style = Style::new()
.fg(dict::WORD_NAME) .fg(theme.dict.word_name)
.bg(name_bg) .bg(name_bg)
.add_modifier(Modifier::BOLD); .add_modifier(Modifier::BOLD);
let alias_style = Style::new().fg(dict::ALIAS).bg(name_bg); let alias_style = Style::new().fg(theme.dict.alias).bg(name_bg);
let name_text = if word.aliases.is_empty() { let name_text = if word.aliases.is_empty() {
format!(" {}", word.name) format!(" {}", word.name)
} else { } else {
@@ -168,19 +171,19 @@ fn render_words(frame: &mut Frame, app: &App, area: Rect, is_searching: bool) {
])); ]));
} }
let stack_style = Style::new().fg(dict::STACK_SIG); let stack_style = Style::new().fg(theme.dict.stack_sig);
lines.push(RLine::from(vec![ lines.push(RLine::from(vec![
Span::raw(" "), Span::raw(" "),
Span::styled(word.stack.to_string(), stack_style), Span::styled(word.stack.to_string(), stack_style),
])); ]));
let desc_style = Style::new().fg(dict::DESCRIPTION); let desc_style = Style::new().fg(theme.dict.description);
lines.push(RLine::from(vec![ lines.push(RLine::from(vec![
Span::raw(" "), Span::raw(" "),
Span::styled(word.desc.to_string(), desc_style), Span::styled(word.desc.to_string(), desc_style),
])); ]));
let example_style = Style::new().fg(dict::EXAMPLE); let example_style = Style::new().fg(theme.dict.example);
lines.push(RLine::from(vec![ lines.push(RLine::from(vec![
Span::raw(" "), Span::raw(" "),
Span::styled(format!("e.g. {}", word.example), example_style), Span::styled(format!("e.g. {}", word.example), example_style),
@@ -206,7 +209,7 @@ fn render_words(frame: &mut Frame, app: &App, area: Rect, is_searching: bool) {
let category = CATEGORIES[app.ui.dict_category]; let category = CATEGORIES[app.ui.dict_category];
format!("{category} ({} words)", words.len()) format!("{category} ({} words)", words.len())
}; };
let border_color = if focused { dict::BORDER_FOCUSED } else { 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))
@@ -216,10 +219,11 @@ fn render_words(frame: &mut Frame, app: &App, area: Rect, is_searching: bool) {
} }
fn render_search_bar(frame: &mut Frame, app: &App, area: Rect) { fn render_search_bar(frame: &mut Frame, app: &App, area: Rect) {
let theme = theme::get();
let style = if app.ui.dict_search_active { let style = if app.ui.dict_search_active {
Style::new().fg(search::ACTIVE) Style::new().fg(theme.search.active)
} else { } else {
Style::new().fg(search::INACTIVE) Style::new().fg(theme.search.inactive)
}; };
let cursor = if app.ui.dict_search_active { "_" } else { "" }; let cursor = if app.ui.dict_search_active { "_" } else { "" };
let text = format!(" /{}{}", app.ui.dict_search_query, cursor); let text = format!(" /{}{}", app.ui.dict_search_query, cursor);

View File

@@ -7,7 +7,7 @@ use ratatui::Frame;
use crate::app::App; use crate::app::App;
use crate::state::{DeviceKind, EngineSection, SettingKind}; use crate::state::{DeviceKind, EngineSection, SettingKind};
use crate::theme::{engine, meter}; use crate::theme;
use crate::widgets::{Orientation, Scope, Spectrum}; use crate::widgets::{Orientation, Scope, Spectrum};
pub fn render(frame: &mut Frame, app: &App, area: Rect) { pub fn render(frame: &mut Frame, app: &App, area: Rect) {
@@ -23,10 +23,11 @@ pub fn render(frame: &mut Frame, app: &App, area: Rect) {
} }
fn render_settings_section(frame: &mut Frame, app: &App, area: Rect) { fn render_settings_section(frame: &mut Frame, app: &App, area: Rect) {
let theme = theme::get();
let block = Block::default() let block = Block::default()
.borders(Borders::ALL) .borders(Borders::ALL)
.title(" Engine ") .title(" Engine ")
.border_style(Style::new().fg(engine::BORDER_MAGENTA)); .border_style(Style::new().fg(theme.engine.border_magenta));
let inner = block.inner(area); let inner = block.inner(area);
frame.render_widget(block, area); frame.render_widget(block, area);
@@ -122,7 +123,7 @@ fn render_settings_section(frame: &mut Frame, app: &App, area: Rect) {
} }
// Scroll indicators // Scroll indicators
let indicator_style = Style::new().fg(engine::SCROLL_INDICATOR); let indicator_style = Style::new().fg(theme.engine.scroll_indicator);
let indicator_x = padded.x + padded.width.saturating_sub(1); let indicator_x = padded.x + padded.width.saturating_sub(1);
if scroll_offset > 0 { if scroll_offset > 0 {
@@ -152,25 +153,27 @@ fn render_visualizers(frame: &mut Frame, app: &App, area: Rect) {
} }
fn render_scope(frame: &mut Frame, app: &App, area: Rect) { fn render_scope(frame: &mut Frame, app: &App, area: Rect) {
let theme = theme::get();
let block = Block::default() let block = Block::default()
.borders(Borders::ALL) .borders(Borders::ALL)
.title(" Scope ") .title(" Scope ")
.border_style(Style::new().fg(engine::BORDER_GREEN)); .border_style(Style::new().fg(theme.engine.border_green));
let inner = block.inner(area); let inner = block.inner(area);
frame.render_widget(block, area); frame.render_widget(block, area);
let scope = Scope::new(&app.metrics.scope) let scope = Scope::new(&app.metrics.scope)
.orientation(Orientation::Horizontal) .orientation(Orientation::Horizontal)
.color(meter::LOW); .color(theme.meter.low);
frame.render_widget(scope, inner); frame.render_widget(scope, inner);
} }
fn render_spectrum(frame: &mut Frame, app: &App, area: Rect) { fn render_spectrum(frame: &mut Frame, app: &App, area: Rect) {
let theme = theme::get();
let block = Block::default() let block = Block::default()
.borders(Borders::ALL) .borders(Borders::ALL)
.title(" Spectrum ") .title(" Spectrum ")
.border_style(Style::new().fg(engine::BORDER_CYAN)); .border_style(Style::new().fg(theme.engine.border_cyan));
let inner = block.inner(area); let inner = block.inner(area);
frame.render_widget(block, area); frame.render_widget(block, area);
@@ -203,25 +206,27 @@ fn devices_section_height(app: &App) -> u16 {
} }
fn render_section_header(frame: &mut Frame, title: &str, focused: bool, area: Rect) { fn render_section_header(frame: &mut Frame, title: &str, focused: bool, area: Rect) {
let theme = theme::get();
let [header_area, divider_area] = let [header_area, divider_area] =
Layout::vertical([Constraint::Length(1), Constraint::Length(1)]).areas(area); Layout::vertical([Constraint::Length(1), Constraint::Length(1)]).areas(area);
let header_style = if focused { let header_style = if focused {
Style::new().fg(engine::HEADER_FOCUSED).add_modifier(Modifier::BOLD) Style::new().fg(theme.engine.header_focused).add_modifier(Modifier::BOLD)
} else { } else {
Style::new().fg(engine::HEADER).add_modifier(Modifier::BOLD) Style::new().fg(theme.engine.header).add_modifier(Modifier::BOLD)
}; };
frame.render_widget(Paragraph::new(title).style(header_style), header_area); frame.render_widget(Paragraph::new(title).style(header_style), header_area);
let divider = "".repeat(area.width as usize); let divider = "".repeat(area.width as usize);
frame.render_widget( frame.render_widget(
Paragraph::new(divider).style(Style::new().fg(engine::DIVIDER)), Paragraph::new(divider).style(Style::new().fg(theme.engine.divider)),
divider_area, divider_area,
); );
} }
fn render_devices(frame: &mut Frame, app: &App, area: Rect) { fn render_devices(frame: &mut Frame, app: &App, area: Rect) {
let theme = theme::get();
let section_focused = app.audio.section == EngineSection::Devices; let section_focused = app.audio.section == EngineSection::Devices;
let [header_area, content_area] = let [header_area, content_area] =
@@ -251,7 +256,7 @@ fn render_devices(frame: &mut Frame, app: &App, area: Rect) {
section_focused, section_focused,
); );
let sep_style = Style::new().fg(engine::SEPARATOR); let sep_style = Style::new().fg(theme.engine.separator);
let sep_lines: Vec<Line> = (0..separator.height) let sep_lines: Vec<Line> = (0..separator.height)
.map(|_| Line::from(Span::styled("", sep_style))) .map(|_| Line::from(Span::styled("", sep_style)))
.collect(); .collect();
@@ -282,15 +287,16 @@ fn render_device_column(
focused: bool, focused: bool,
section_focused: bool, section_focused: bool,
) { ) {
let theme = theme::get();
let [label_area, list_area] = let [label_area, list_area] =
Layout::vertical([Constraint::Length(1), Constraint::Min(1)]).areas(area); Layout::vertical([Constraint::Length(1), Constraint::Min(1)]).areas(area);
let label_style = if focused { let label_style = if focused {
Style::new().fg(engine::FOCUSED).add_modifier(Modifier::BOLD) Style::new().fg(theme.engine.focused).add_modifier(Modifier::BOLD)
} else if section_focused { } else if section_focused {
Style::new().fg(engine::LABEL_FOCUSED) Style::new().fg(theme.engine.label_focused)
} else { } else {
Style::new().fg(engine::LABEL_DIM) Style::new().fg(theme.engine.label_dim)
}; };
let arrow = if focused { "> " } else { " " }; let arrow = if focused { "> " } else { " " };
@@ -308,6 +314,7 @@ fn render_device_column(
} }
fn render_settings(frame: &mut Frame, app: &App, area: Rect) { fn render_settings(frame: &mut Frame, app: &App, area: Rect) {
let theme = theme::get();
let section_focused = app.audio.section == EngineSection::Settings; let section_focused = app.audio.section == EngineSection::Settings;
let [header_area, content_area] = let [header_area, content_area] =
@@ -315,10 +322,10 @@ fn render_settings(frame: &mut Frame, app: &App, area: Rect) {
render_section_header(frame, "SETTINGS", section_focused, header_area); render_section_header(frame, "SETTINGS", section_focused, header_area);
let highlight = Style::new().fg(engine::FOCUSED).add_modifier(Modifier::BOLD); let highlight = Style::new().fg(theme.engine.focused).add_modifier(Modifier::BOLD);
let normal = Style::new().fg(engine::NORMAL); let normal = Style::new().fg(theme.engine.normal);
let label_style = Style::new().fg(engine::LABEL); let label_style = Style::new().fg(theme.engine.label);
let value_style = Style::new().fg(engine::VALUE); let value_style = Style::new().fg(theme.engine.value);
let channels_focused = section_focused && app.audio.setting_kind == SettingKind::Channels; let channels_focused = section_focused && app.audio.setting_kind == SettingKind::Channels;
let buffer_focused = section_focused && app.audio.setting_kind == SettingKind::BufferSize; let buffer_focused = section_focused && app.audio.setting_kind == SettingKind::BufferSize;
@@ -420,6 +427,7 @@ fn render_settings(frame: &mut Frame, app: &App, area: Rect) {
} }
fn render_samples(frame: &mut Frame, app: &App, area: Rect) { fn render_samples(frame: &mut Frame, app: &App, area: Rect) {
let theme = theme::get();
let section_focused = app.audio.section == EngineSection::Samples; let section_focused = app.audio.section == EngineSection::Samples;
let [header_area, content_area, _, hint_area] = Layout::vertical([ let [header_area, content_area, _, hint_area] = Layout::vertical([
@@ -435,8 +443,8 @@ fn render_samples(frame: &mut Frame, app: &App, area: Rect) {
let header_text = format!("SAMPLES {path_count} paths · {sample_count} indexed"); let header_text = format!("SAMPLES {path_count} paths · {sample_count} indexed");
render_section_header(frame, &header_text, section_focused, header_area); render_section_header(frame, &header_text, section_focused, header_area);
let dim = Style::new().fg(engine::DIM); let dim = Style::new().fg(theme.engine.dim);
let path_style = Style::new().fg(engine::PATH); let path_style = Style::new().fg(theme.engine.path);
let mut lines: Vec<Line> = Vec::new(); let mut lines: Vec<Line> = Vec::new();
if app.audio.config.sample_paths.is_empty() { if app.audio.config.sample_paths.is_empty() {
@@ -467,15 +475,15 @@ fn render_samples(frame: &mut Frame, app: &App, area: Rect) {
frame.render_widget(Paragraph::new(lines), content_area); frame.render_widget(Paragraph::new(lines), content_area);
let hint_style = if section_focused { let hint_style = if section_focused {
Style::new().fg(engine::HINT_ACTIVE) Style::new().fg(theme.engine.hint_active)
} else { } else {
Style::new().fg(engine::HINT_INACTIVE) Style::new().fg(theme.engine.hint_inactive)
}; };
let hint = Line::from(vec![ let hint = Line::from(vec![
Span::styled("A", hint_style), Span::styled("A", hint_style),
Span::styled(":add ", Style::new().fg(engine::DIM)), Span::styled(":add ", Style::new().fg(theme.engine.dim)),
Span::styled("D", hint_style), Span::styled("D", hint_style),
Span::styled(":remove", Style::new().fg(engine::DIM)), Span::styled(":remove", Style::new().fg(theme.engine.dim)),
]); ]);
frame.render_widget(Paragraph::new(hint), hint_area); frame.render_widget(Paragraph::new(hint), hint_area);
} }

View File

@@ -7,7 +7,7 @@ use ratatui::Frame;
use tui_big_text::{BigText, PixelSize}; use tui_big_text::{BigText, PixelSize};
use crate::app::App; use crate::app::App;
use crate::theme::{dict, markdown, search, ui}; use crate::theme;
use crate::views::highlight; use crate::views::highlight;
// To add a new help topic: drop a .md file in docs/ and add one line here. // To add a new help topic: drop a .md file in docs/ and add one line here.
@@ -32,22 +32,28 @@ pub fn render(frame: &mut Frame, app: &App, area: Rect) {
} }
fn render_topics(frame: &mut Frame, app: &App, area: Rect) { fn render_topics(frame: &mut Frame, app: &App, area: Rect) {
let theme = theme::get();
let items: Vec<ListItem> = DOCS let items: Vec<ListItem> = DOCS
.iter() .iter()
.enumerate() .enumerate()
.map(|(i, (name, _))| { .map(|(i, (name, _))| {
let selected = i == app.ui.help_topic; let selected = i == app.ui.help_topic;
let style = if selected { let style = if selected {
Style::new().fg(dict::CATEGORY_SELECTED).add_modifier(Modifier::BOLD) Style::new().fg(theme.dict.category_selected).add_modifier(Modifier::BOLD)
} else { } else {
Style::new().fg(ui::TEXT_PRIMARY) Style::new().fg(theme.ui.text_primary)
}; };
let prefix = if selected { "> " } else { " " }; let prefix = if selected { "> " } else { " " };
ListItem::new(format!("{prefix}{name}")).style(style) ListItem::new(format!("{prefix}{name}")).style(style)
}) })
.collect(); .collect();
let list = List::new(items).block(Block::default().borders(Borders::ALL).title("Topics")); let list = List::new(items).block(
Block::default()
.borders(Borders::ALL)
.border_style(Style::new().fg(theme.dict.border_focused))
.title("Topics"),
);
frame.render_widget(list, area); frame.render_widget(list, area);
} }
@@ -55,6 +61,7 @@ const WELCOME_TOPIC: usize = 0;
const BIG_TITLE_HEIGHT: u16 = 6; const BIG_TITLE_HEIGHT: u16 = 6;
fn render_content(frame: &mut Frame, app: &App, area: Rect) { fn render_content(frame: &mut Frame, app: &App, area: Rect) {
let theme = theme::get();
let (_, md) = DOCS[app.ui.help_topic]; let (_, md) = DOCS[app.ui.help_topic];
let is_welcome = app.ui.help_topic == WELCOME_TOPIC; let is_welcome = app.ui.help_topic == WELCOME_TOPIC;
@@ -64,13 +71,13 @@ fn render_content(frame: &mut Frame, app: &App, area: Rect) {
.areas(area); .areas(area);
let big_title = BigText::builder() let big_title = BigText::builder()
.pixel_size(PixelSize::Quadrant) .pixel_size(PixelSize::Quadrant)
.style(Style::new().fg(markdown::H1).bold()) .style(Style::new().fg(theme.markdown.h1).bold())
.lines(vec!["CAGIRE".into()]) .lines(vec!["CAGIRE".into()])
.centered() .centered()
.build(); .build();
let subtitle = Paragraph::new(RLine::from(Span::styled( let subtitle = Paragraph::new(RLine::from(Span::styled(
"A Forth Sequencer", "A Forth Sequencer",
Style::new().fg(ui::TEXT_PRIMARY), Style::new().fg(theme.ui.text_primary),
))) )))
.alignment(ratatui::layout::Alignment::Center); .alignment(ratatui::layout::Alignment::Center);
let [big_area, subtitle_area] = let [big_area, subtitle_area] =
@@ -119,6 +126,7 @@ fn render_content(frame: &mut Frame, app: &App, area: Rect) {
.block( .block(
Block::default() Block::default()
.borders(Borders::ALL) .borders(Borders::ALL)
.border_style(Style::new().fg(theme.ui.border))
.padding(Padding::new(2, 2, 2, 2)), .padding(Padding::new(2, 2, 2, 2)),
) )
.wrap(Wrap { trim: false }); .wrap(Wrap { trim: false });
@@ -126,10 +134,11 @@ fn render_content(frame: &mut Frame, app: &App, area: Rect) {
} }
fn render_search_bar(frame: &mut Frame, app: &App, area: Rect) { fn render_search_bar(frame: &mut Frame, app: &App, area: Rect) {
let theme = theme::get();
let style = if app.ui.help_search_active { let style = if app.ui.help_search_active {
Style::new().fg(search::ACTIVE) Style::new().fg(theme.search.active)
} else { } else {
Style::new().fg(search::INACTIVE) Style::new().fg(theme.search.inactive)
}; };
let cursor = if app.ui.help_search_active { "" } else { "" }; let cursor = if app.ui.help_search_active { "" } else { "" };
let text = format!(" /{}{cursor}", app.ui.help_search_query); let text = format!(" /{}{cursor}", app.ui.help_search_query);
@@ -137,6 +146,7 @@ fn render_search_bar(frame: &mut Frame, app: &App, area: Rect) {
} }
fn highlight_line<'a>(line: RLine<'a>, query: &str) -> RLine<'a> { fn highlight_line<'a>(line: RLine<'a>, query: &str) -> RLine<'a> {
let theme = theme::get();
let mut result: Vec<Span<'a>> = Vec::new(); let mut result: Vec<Span<'a>> = Vec::new();
for span in line.spans { for span in line.spans {
let lower = span.content.to_lowercase(); let lower = span.content.to_lowercase();
@@ -146,7 +156,7 @@ fn highlight_line<'a>(line: RLine<'a>, query: &str) -> RLine<'a> {
} }
let content = span.content.to_string(); let content = span.content.to_string();
let base_style = span.style; let base_style = span.style;
let hl_style = base_style.bg(search::MATCH_BG).fg(search::MATCH_FG); let hl_style = base_style.bg(theme.search.match_bg).fg(theme.search.match_fg);
let mut start = 0; let mut start = 0;
let lower_bytes = lower.as_bytes(); let lower_bytes = lower.as_bytes();
let query_bytes = query.as_bytes(); let query_bytes = query.as_bytes();
@@ -186,7 +196,8 @@ pub fn find_match(query: &str) -> Option<(usize, usize)> {
} }
fn code_border_style() -> Style { fn code_border_style() -> Style {
Style::new().fg(markdown::CODE_BORDER) let theme = theme::get();
Style::new().fg(theme.markdown.code_border)
} }
fn preprocess_underscores(md: &str) -> String { fn preprocess_underscores(md: &str) -> String {
@@ -269,16 +280,17 @@ fn parse_markdown(md: &str) -> Vec<RLine<'static>> {
} }
fn composite_to_line(composite: Composite) -> RLine<'static> { fn composite_to_line(composite: Composite) -> RLine<'static> {
let theme = theme::get();
let base_style = match composite.style { let base_style = match composite.style {
CompositeStyle::Header(1) => Style::new() CompositeStyle::Header(1) => Style::new()
.fg(markdown::H1) .fg(theme.markdown.h1)
.add_modifier(Modifier::BOLD | Modifier::UNDERLINED), .add_modifier(Modifier::BOLD | Modifier::UNDERLINED),
CompositeStyle::Header(2) => Style::new().fg(markdown::H2).add_modifier(Modifier::BOLD), CompositeStyle::Header(2) => Style::new().fg(theme.markdown.h2).add_modifier(Modifier::BOLD),
CompositeStyle::Header(_) => Style::new().fg(markdown::H3).add_modifier(Modifier::BOLD), CompositeStyle::Header(_) => Style::new().fg(theme.markdown.h3).add_modifier(Modifier::BOLD),
CompositeStyle::ListItem(_) => Style::new().fg(markdown::LIST), CompositeStyle::ListItem(_) => Style::new().fg(theme.markdown.list),
CompositeStyle::Quote => Style::new().fg(markdown::QUOTE), CompositeStyle::Quote => Style::new().fg(theme.markdown.quote),
CompositeStyle::Code => Style::new().fg(markdown::CODE), CompositeStyle::Code => Style::new().fg(theme.markdown.code),
CompositeStyle::Paragraph => Style::new().fg(markdown::TEXT), CompositeStyle::Paragraph => Style::new().fg(theme.markdown.text),
}; };
let prefix = match composite.style { let prefix = match composite.style {
@@ -300,6 +312,7 @@ fn composite_to_line(composite: Composite) -> RLine<'static> {
} }
fn compound_to_spans(compound: Compound, base: Style, out: &mut Vec<Span<'static>>) { fn compound_to_spans(compound: Compound, base: Style, out: &mut Vec<Span<'static>>) {
let theme = theme::get();
let mut style = base; let mut style = base;
if compound.bold { if compound.bold {
@@ -309,7 +322,7 @@ fn compound_to_spans(compound: Compound, base: Style, out: &mut Vec<Span<'static
style = style.add_modifier(Modifier::ITALIC); style = style.add_modifier(Modifier::ITALIC);
} }
if compound.code { if compound.code {
style = Style::new().fg(markdown::CODE); style = Style::new().fg(theme.markdown.code);
} }
if compound.strikeout { if compound.strikeout {
style = style.add_modifier(Modifier::CROSSED_OUT); style = style.add_modifier(Modifier::CROSSED_OUT);
@@ -317,7 +330,7 @@ fn compound_to_spans(compound: Compound, base: Style, out: &mut Vec<Span<'static
let src = compound.src.to_string(); let src = compound.src.to_string();
let link_style = Style::new() let link_style = Style::new()
.fg(markdown::LINK) .fg(theme.markdown.link)
.add_modifier(Modifier::UNDERLINED); .add_modifier(Modifier::UNDERLINED);
let mut rest = src.as_str(); let mut rest = src.as_str();
@@ -337,7 +350,7 @@ fn compound_to_spans(compound: Compound, base: Style, out: &mut Vec<Span<'static
out.push(Span::styled(text.to_string(), link_style)); out.push(Span::styled(text.to_string(), link_style));
out.push(Span::styled( out.push(Span::styled(
format!(" ({url})"), format!(" ({url})"),
Style::new().fg(markdown::LINK_URL), Style::new().fg(theme.markdown.link_url),
)); ));
} }
rest = &rest[url_start + url_end + 1..]; rest = &rest[url_start + url_end + 1..];

View File

@@ -1,7 +1,7 @@
use ratatui::style::{Modifier, Style}; use ratatui::style::{Modifier, Style};
use crate::model::{SourceSpan, WordCompile, WORDS}; use crate::model::{SourceSpan, WordCompile, WORDS};
use crate::theme::syntax; use crate::theme;
#[derive(Clone, Copy, PartialEq, Eq)] #[derive(Clone, Copy, PartialEq, Eq)]
pub enum TokenKind { pub enum TokenKind {
@@ -25,23 +25,24 @@ pub enum TokenKind {
impl TokenKind { impl TokenKind {
pub fn style(self) -> Style { pub fn style(self) -> Style {
let theme = theme::get();
let (fg, bg) = match self { let (fg, bg) = match self {
TokenKind::Emit => syntax::EMIT, TokenKind::Emit => theme.syntax.emit,
TokenKind::Number => syntax::NUMBER, TokenKind::Number => theme.syntax.number,
TokenKind::String => syntax::STRING, TokenKind::String => theme.syntax.string,
TokenKind::Comment => syntax::COMMENT, TokenKind::Comment => theme.syntax.comment,
TokenKind::Keyword => syntax::KEYWORD, TokenKind::Keyword => theme.syntax.keyword,
TokenKind::StackOp => syntax::STACK_OP, TokenKind::StackOp => theme.syntax.stack_op,
TokenKind::Operator => syntax::OPERATOR, TokenKind::Operator => theme.syntax.operator,
TokenKind::Sound => syntax::SOUND, TokenKind::Sound => theme.syntax.sound,
TokenKind::Param => syntax::PARAM, TokenKind::Param => theme.syntax.param,
TokenKind::Context => syntax::CONTEXT, TokenKind::Context => theme.syntax.context,
TokenKind::Note => syntax::NOTE, TokenKind::Note => theme.syntax.note,
TokenKind::Interval => syntax::INTERVAL, TokenKind::Interval => theme.syntax.interval,
TokenKind::Variable => syntax::VARIABLE, TokenKind::Variable => theme.syntax.variable,
TokenKind::Vary => syntax::VARY, TokenKind::Vary => theme.syntax.vary,
TokenKind::Generator => syntax::GENERATOR, TokenKind::Generator => theme.syntax.generator,
TokenKind::Default => syntax::DEFAULT, TokenKind::Default => theme.syntax.default,
}; };
let style = Style::default().fg(fg).bg(bg); let style = Style::default().fg(fg).bg(bg);
if matches!(self, TokenKind::Emit) { if matches!(self, TokenKind::Emit) {
@@ -52,7 +53,8 @@ impl TokenKind {
} }
pub fn gap_style() -> Style { pub fn gap_style() -> Style {
Style::default().bg(syntax::GAP_BG) let theme = theme::get();
Style::default().bg(theme.syntax.gap_bg)
} }
} }
@@ -223,10 +225,11 @@ pub fn highlight_line_with_runtime(
if token.varargs { if token.varargs {
style = style.add_modifier(Modifier::UNDERLINED); style = style.add_modifier(Modifier::UNDERLINED);
} }
let theme = theme::get();
if is_selected { if is_selected {
style = style.bg(syntax::SELECTED_BG).add_modifier(Modifier::BOLD); style = style.bg(theme.syntax.selected_bg).add_modifier(Modifier::BOLD);
} else if is_executed { } else if is_executed {
style = style.bg(syntax::EXECUTED_BG); style = style.bg(theme.syntax.executed_bg);
} }
result.push((style, line[token.start..token.end].to_string())); result.push((style, line[token.start..token.end].to_string()));

View File

@@ -5,7 +5,7 @@ use ratatui::Frame;
use crate::app::App; use crate::app::App;
use crate::engine::SequencerSnapshot; use crate::engine::SequencerSnapshot;
use crate::theme::{meter, selection, tile, ui}; use crate::theme;
use crate::widgets::{Orientation, Scope, Spectrum, VuMeter}; use crate::widgets::{Orientation, Scope, Spectrum, VuMeter};
pub fn render(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area: Rect) { pub fn render(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area: Rect) {
@@ -65,10 +65,12 @@ pub fn render(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area:
const STEPS_PER_PAGE: usize = 32; const STEPS_PER_PAGE: usize = 32;
fn render_sequencer(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area: Rect) { fn render_sequencer(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area: Rect) {
let theme = theme::get();
if area.width < 50 { if area.width < 50 {
let msg = Paragraph::new("Terminal too narrow") let msg = Paragraph::new("Terminal too narrow")
.alignment(Alignment::Center) .alignment(Alignment::Center)
.style(Style::new().fg(ui::TEXT_MUTED)); .style(Style::new().fg(theme.ui.text_muted));
frame.render_widget(msg, area); frame.render_widget(msg, area);
return; return;
} }
@@ -132,6 +134,7 @@ fn render_tile(
snapshot: &SequencerSnapshot, snapshot: &SequencerSnapshot,
step_idx: usize, step_idx: usize,
) { ) {
let theme = theme::get();
let pattern = app.current_edit_pattern(); let pattern = app.current_edit_pattern();
let step = pattern.step(step_idx); let step = pattern.step(step_idx);
let is_active = step.map(|s| s.active).unwrap_or(false); let is_active = step.map(|s| s.active).unwrap_or(false);
@@ -149,26 +152,26 @@ fn render_tile(
let link_color = step.and_then(|s| s.source).map(|src| { let link_color = step.and_then(|s| s.source).map(|src| {
let i = src % 5; let i = src % 5;
(tile::LINK_BRIGHT[i], tile::LINK_DIM[i]) (theme.tile.link_bright[i], theme.tile.link_dim[i])
}); });
let (bg, fg) = match (is_playing, is_active, is_selected, is_linked, in_selection) { let (bg, fg) = match (is_playing, is_active, is_selected, is_linked, in_selection) {
(true, true, _, _, _) => (tile::PLAYING_ACTIVE_BG, tile::PLAYING_ACTIVE_FG), (true, true, _, _, _) => (theme.tile.playing_active_bg, theme.tile.playing_active_fg),
(true, false, _, _, _) => (tile::PLAYING_INACTIVE_BG, tile::PLAYING_INACTIVE_FG), (true, false, _, _, _) => (theme.tile.playing_inactive_bg, theme.tile.playing_inactive_fg),
(false, true, true, true, _) => { (false, true, true, true, _) => {
let (r, g, b) = link_color.unwrap().0; let (r, g, b) = link_color.unwrap().0;
(Color::Rgb(r, g, b), selection::CURSOR_FG) (Color::Rgb(r, g, b), theme.selection.cursor_fg)
} }
(false, true, true, false, _) => (tile::ACTIVE_SELECTED_BG, selection::CURSOR_FG), (false, true, true, false, _) => (theme.tile.active_selected_bg, theme.selection.cursor_fg),
(false, true, _, _, true) => (tile::ACTIVE_IN_RANGE_BG, selection::CURSOR_FG), (false, true, _, _, true) => (theme.tile.active_in_range_bg, theme.selection.cursor_fg),
(false, true, false, true, _) => { (false, true, false, true, _) => {
let (r, g, b) = link_color.unwrap().1; let (r, g, b) = link_color.unwrap().1;
(Color::Rgb(r, g, b), tile::ACTIVE_FG) (Color::Rgb(r, g, b), theme.tile.active_fg)
} }
(false, true, false, false, _) => (tile::ACTIVE_BG, tile::ACTIVE_FG), (false, true, false, false, _) => (theme.tile.active_bg, theme.tile.active_fg),
(false, false, true, _, _) => (selection::SELECTED, selection::CURSOR_FG), (false, false, true, _, _) => (theme.selection.selected, theme.selection.cursor_fg),
(false, false, _, _, true) => (selection::IN_RANGE, selection::CURSOR_FG), (false, false, _, _, true) => (theme.selection.in_range, theme.selection.cursor_fg),
(false, false, false, _, _) => (tile::INACTIVE_BG, tile::INACTIVE_FG), (false, false, false, _, _) => (theme.tile.inactive_bg, theme.tile.inactive_fg),
}; };
let source_idx = step.and_then(|s| s.source); let source_idx = step.and_then(|s| s.source);
@@ -231,9 +234,10 @@ fn render_tile(
} }
fn render_scope(frame: &mut Frame, app: &App, area: Rect) { fn render_scope(frame: &mut Frame, app: &App, area: Rect) {
let theme = theme::get();
let scope = Scope::new(&app.metrics.scope) let scope = Scope::new(&app.metrics.scope)
.orientation(Orientation::Horizontal) .orientation(Orientation::Horizontal)
.color(meter::LOW); .color(theme.meter.low);
frame.render_widget(scope, area); frame.render_widget(scope, area);
} }
@@ -247,4 +251,3 @@ fn render_vu_meter(frame: &mut Frame, app: &App, area: Rect) {
let vu = VuMeter::new(app.metrics.peak_left, app.metrics.peak_right); let vu = VuMeter::new(app.metrics.peak_left, app.metrics.peak_right);
frame.render_widget(vu, area); frame.render_widget(vu, area);
} }

View File

@@ -7,13 +7,15 @@ use ratatui::Frame;
use crate::app::App; use crate::app::App;
use crate::engine::LinkState; use crate::engine::LinkState;
use crate::state::OptionsFocus; use crate::state::OptionsFocus;
use crate::theme::{hint, link_status, modal, ui, values}; use crate::theme;
pub fn render(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) { pub fn render(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) {
let theme = theme::get();
let block = Block::default() let block = Block::default()
.borders(Borders::ALL) .borders(Borders::ALL)
.title(" Options ") .title(" Options ")
.border_style(Style::new().fg(modal::INPUT)); .border_style(Style::new().fg(theme.modal.input));
let inner = block.inner(area); let inner = block.inner(area);
frame.render_widget(block, area); frame.render_widget(block, area);
@@ -32,11 +34,11 @@ pub fn render(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) {
let enabled = link.is_enabled(); let enabled = link.is_enabled();
let peers = link.peers(); let peers = link.peers();
let (status_text, status_color) = if !enabled { let (status_text, status_color) = if !enabled {
("DISABLED", link_status::DISABLED) ("DISABLED", theme.link_status.disabled)
} else if peers > 0 { } else if peers > 0 {
("CONNECTED", link_status::CONNECTED) ("CONNECTED", theme.link_status.connected)
} else { } else {
("LISTENING", link_status::LISTENING) ("LISTENING", theme.link_status.listening)
}; };
let peer_text = if enabled && peers > 0 { let peer_text = if enabled && peers > 0 {
if peers == 1 { if peers == 1 {
@@ -51,14 +53,14 @@ pub fn render(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) {
let link_header = Line::from(vec![ let link_header = Line::from(vec![
Span::styled( Span::styled(
"ABLETON LINK", "ABLETON LINK",
Style::new().fg(ui::HEADER).add_modifier(Modifier::BOLD), Style::new().fg(theme.ui.header).add_modifier(Modifier::BOLD),
), ),
Span::raw(" "), Span::raw(" "),
Span::styled( Span::styled(
status_text, status_text,
Style::new().fg(status_color).add_modifier(Modifier::BOLD), Style::new().fg(status_color).add_modifier(Modifier::BOLD),
), ),
Span::styled(peer_text, Style::new().fg(ui::TEXT_MUTED)), Span::styled(peer_text, Style::new().fg(theme.ui.text_muted)),
]); ]);
// Prepare values // Prepare values
@@ -68,28 +70,37 @@ pub fn render(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) {
let beat_str = format!("{:.2}", link.beat()); let beat_str = format!("{:.2}", link.beat());
let phase_str = format!("{:.2}", link.phase()); let phase_str = format!("{:.2}", link.phase());
let tempo_style = Style::new().fg(values::TEMPO).add_modifier(Modifier::BOLD); let tempo_style = Style::new().fg(theme.values.tempo).add_modifier(Modifier::BOLD);
let value_style = Style::new().fg(values::VALUE); let value_style = Style::new().fg(theme.values.value);
// Build flat list of all lines // Build flat list of all lines
let lines: Vec<Line> = vec![ let lines: Vec<Line> = vec![
// DISPLAY section (lines 0-7) // DISPLAY section (lines 0-8)
render_section_header("DISPLAY"), render_section_header("DISPLAY", &theme),
render_divider(content_width), render_divider(content_width, &theme),
render_option_line(
"Theme",
app.ui.color_scheme.label(),
focus == OptionsFocus::ColorScheme,
&theme,
),
render_option_line( render_option_line(
"Refresh rate", "Refresh rate",
app.audio.config.refresh_rate.label(), app.audio.config.refresh_rate.label(),
focus == OptionsFocus::RefreshRate, focus == OptionsFocus::RefreshRate,
&theme,
), ),
render_option_line( render_option_line(
"Runtime highlight", "Runtime highlight",
if app.ui.runtime_highlight { "On" } else { "Off" }, if app.ui.runtime_highlight { "On" } else { "Off" },
focus == OptionsFocus::RuntimeHighlight, focus == OptionsFocus::RuntimeHighlight,
&theme,
), ),
render_option_line( render_option_line(
"Show scope", "Show scope",
if app.audio.config.show_scope { "On" } else { "Off" }, if app.audio.config.show_scope { "On" } else { "Off" },
focus == OptionsFocus::ShowScope, focus == OptionsFocus::ShowScope,
&theme,
), ),
render_option_line( render_option_line(
"Show spectrum", "Show spectrum",
@@ -99,22 +110,25 @@ pub fn render(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) {
"Off" "Off"
}, },
focus == OptionsFocus::ShowSpectrum, focus == OptionsFocus::ShowSpectrum,
&theme,
), ),
render_option_line( render_option_line(
"Completion", "Completion",
if app.ui.show_completion { "On" } else { "Off" }, if app.ui.show_completion { "On" } else { "Off" },
focus == OptionsFocus::ShowCompletion, focus == OptionsFocus::ShowCompletion,
&theme,
), ),
render_option_line("Flash brightness", &flash_str, focus == OptionsFocus::FlashBrightness), render_option_line("Flash brightness", &flash_str, focus == OptionsFocus::FlashBrightness, &theme),
// Blank line (line 8) // Blank line (line 9)
Line::from(""), Line::from(""),
// ABLETON LINK section (lines 9-14) // ABLETON LINK section (lines 10-15)
link_header, link_header,
render_divider(content_width), render_divider(content_width, &theme),
render_option_line( render_option_line(
"Enabled", "Enabled",
if link.is_enabled() { "On" } else { "Off" }, if link.is_enabled() { "On" } else { "Off" },
focus == OptionsFocus::LinkEnabled, focus == OptionsFocus::LinkEnabled,
&theme,
), ),
render_option_line( render_option_line(
"Start/Stop sync", "Start/Stop sync",
@@ -124,16 +138,17 @@ pub fn render(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) {
"Off" "Off"
}, },
focus == OptionsFocus::StartStopSync, focus == OptionsFocus::StartStopSync,
&theme,
), ),
render_option_line("Quantum", &quantum_str, focus == OptionsFocus::Quantum), render_option_line("Quantum", &quantum_str, focus == OptionsFocus::Quantum, &theme),
// Blank line (line 15) // Blank line (line 16)
Line::from(""), Line::from(""),
// SESSION section (lines 16-21) // SESSION section (lines 17-22)
render_section_header("SESSION"), render_section_header("SESSION", &theme),
render_divider(content_width), render_divider(content_width, &theme),
render_readonly_line("Tempo", &tempo_str, tempo_style), render_readonly_line("Tempo", &tempo_str, tempo_style, &theme),
render_readonly_line("Beat", &beat_str, value_style), render_readonly_line("Beat", &beat_str, value_style, &theme),
render_readonly_line("Phase", &phase_str, value_style), render_readonly_line("Phase", &phase_str, value_style, &theme),
]; ];
let total_lines = lines.len(); let total_lines = lines.len();
@@ -141,15 +156,16 @@ pub fn render(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) {
// Map focus to line index // Map focus to line index
let focus_line: usize = match focus { let focus_line: usize = match focus {
OptionsFocus::RefreshRate => 2, OptionsFocus::ColorScheme => 2,
OptionsFocus::RuntimeHighlight => 3, OptionsFocus::RefreshRate => 3,
OptionsFocus::ShowScope => 4, OptionsFocus::RuntimeHighlight => 4,
OptionsFocus::ShowSpectrum => 5, OptionsFocus::ShowScope => 5,
OptionsFocus::ShowCompletion => 6, OptionsFocus::ShowSpectrum => 6,
OptionsFocus::FlashBrightness => 7, OptionsFocus::ShowCompletion => 7,
OptionsFocus::LinkEnabled => 11, OptionsFocus::FlashBrightness => 8,
OptionsFocus::StartStopSync => 12, OptionsFocus::LinkEnabled => 12,
OptionsFocus::Quantum => 13, OptionsFocus::StartStopSync => 13,
OptionsFocus::Quantum => 14,
}; };
// Calculate scroll offset to keep focused line visible (centered when possible) // Calculate scroll offset to keep focused line visible (centered when possible)
@@ -172,7 +188,7 @@ pub fn render(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) {
frame.render_widget(Paragraph::new(visible_lines), padded); frame.render_widget(Paragraph::new(visible_lines), padded);
// Render scroll indicators // Render scroll indicators
let indicator_style = Style::new().fg(ui::TEXT_DIM); let indicator_style = Style::new().fg(theme.ui.text_dim);
let indicator_x = padded.x + padded.width.saturating_sub(1); let indicator_x = padded.x + padded.width.saturating_sub(1);
if scroll_offset > 0 { if scroll_offset > 0 {
@@ -192,24 +208,24 @@ pub fn render(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) {
} }
} }
fn render_section_header(title: &str) -> Line<'static> { fn render_section_header(title: &str, theme: &theme::ThemeColors) -> Line<'static> {
Line::from(Span::styled( Line::from(Span::styled(
title.to_string(), title.to_string(),
Style::new().fg(ui::HEADER).add_modifier(Modifier::BOLD), Style::new().fg(theme.ui.header).add_modifier(Modifier::BOLD),
)) ))
} }
fn render_divider(width: usize) -> Line<'static> { fn render_divider(width: usize, theme: &theme::ThemeColors) -> Line<'static> {
Line::from(Span::styled( Line::from(Span::styled(
"".repeat(width), "".repeat(width),
Style::new().fg(ui::BORDER), Style::new().fg(theme.ui.border),
)) ))
} }
fn render_option_line<'a>(label: &'a str, value: &'a str, focused: bool) -> Line<'a> { fn render_option_line<'a>(label: &'a str, value: &'a str, focused: bool, theme: &theme::ThemeColors) -> Line<'a> {
let highlight = Style::new().fg(hint::KEY).add_modifier(Modifier::BOLD); let highlight = Style::new().fg(theme.hint.key).add_modifier(Modifier::BOLD);
let normal = Style::new().fg(ui::TEXT_PRIMARY); let normal = Style::new().fg(theme.ui.text_primary);
let label_style = Style::new().fg(ui::TEXT_MUTED); let label_style = Style::new().fg(theme.ui.text_muted);
let prefix = if focused { "> " } else { " " }; let prefix = if focused { "> " } else { " " };
let prefix_style = if focused { highlight } else { normal }; let prefix_style = if focused { highlight } else { normal };
@@ -231,8 +247,8 @@ fn render_option_line<'a>(label: &'a str, value: &'a str, focused: bool) -> Line
]) ])
} }
fn render_readonly_line<'a>(label: &'a str, value: &'a str, value_style: Style) -> Line<'a> { fn render_readonly_line<'a>(label: &'a str, value: &'a str, value_style: Style, theme: &theme::ThemeColors) -> Line<'a> {
let label_style = Style::new().fg(ui::TEXT_MUTED); let label_style = Style::new().fg(theme.ui.text_muted);
let label_width = 20; let label_width = 20;
let padded_label = format!("{label:<label_width$}"); let padded_label = format!("{label:<label_width$}");

View File

@@ -8,7 +8,7 @@ use crate::app::App;
use crate::engine::SequencerSnapshot; use crate::engine::SequencerSnapshot;
use crate::model::{MAX_BANKS, MAX_PATTERNS}; use crate::model::{MAX_BANKS, MAX_PATTERNS};
use crate::state::PatternsColumn; use crate::state::PatternsColumn;
use crate::theme::{list, selection, ui}; use crate::theme;
const MIN_ROW_HEIGHT: u16 = 1; const MIN_ROW_HEIGHT: u16 = 1;
@@ -27,12 +27,13 @@ pub fn render(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area:
} }
fn render_banks(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area: Rect) { fn render_banks(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area: Rect) {
let theme = theme::get();
let is_focused = matches!(app.patterns_nav.column, PatternsColumn::Banks); let is_focused = matches!(app.patterns_nav.column, PatternsColumn::Banks);
let [title_area, inner] = let [title_area, inner] =
Layout::vertical([Constraint::Length(1), Constraint::Fill(1)]).areas(area); Layout::vertical([Constraint::Length(1), Constraint::Fill(1)]).areas(area);
let title_color = if is_focused { ui::HEADER } else { ui::UNFOCUSED }; let title_color = if is_focused { theme.ui.header } else { theme.ui.unfocused };
let title = Paragraph::new("Banks") let title = Paragraph::new("Banks")
.style(Style::new().fg(title_color)) .style(Style::new().fg(title_color))
.alignment(ratatui::layout::Alignment::Center); .alignment(ratatui::layout::Alignment::Center);
@@ -91,12 +92,12 @@ fn render_banks(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area
let is_staged = banks_with_staged.contains(&idx); let is_staged = banks_with_staged.contains(&idx);
let (bg, fg, prefix) = match (is_cursor, is_playing, is_staged) { let (bg, fg, prefix) = match (is_cursor, is_playing, is_staged) {
(true, _, _) => (selection::CURSOR, selection::CURSOR_FG, ""), (true, _, _) => (theme.selection.cursor, theme.selection.cursor_fg, ""),
(false, true, _) => (list::PLAYING_BG, list::PLAYING_FG, "> "), (false, true, _) => (theme.list.playing_bg, theme.list.playing_fg, "> "),
(false, false, true) => (list::STAGED_PLAY_BG, list::STAGED_PLAY_FG, "+ "), (false, false, true) => (theme.list.staged_play_bg, theme.list.staged_play_fg, "+ "),
(false, false, false) if is_selected => (list::HOVER_BG, list::HOVER_FG, ""), (false, false, false) if is_selected => (theme.list.hover_bg, theme.list.hover_fg, ""),
(false, false, false) if is_edit => (list::EDIT_BG, list::EDIT_FG, ""), (false, false, false) if is_edit => (theme.list.edit_bg, theme.list.edit_fg, ""),
(false, false, false) => (ui::BG, ui::TEXT_MUTED, ""), (false, false, false) => (theme.ui.bg, theme.ui.text_muted, ""),
}; };
let name = app.project_state.project.banks[idx] let name = app.project_state.project.banks[idx]
@@ -136,7 +137,7 @@ fn render_banks(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area
} }
// Scroll indicators // Scroll indicators
let indicator_style = Style::new().fg(ui::TEXT_MUTED); let indicator_style = Style::new().fg(theme.ui.text_muted);
if scroll_offset > 0 { if scroll_offset > 0 {
let indicator = Paragraph::new("") let indicator = Paragraph::new("")
.style(indicator_style) .style(indicator_style)
@@ -155,12 +156,13 @@ fn render_banks(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area
fn render_patterns(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area: Rect) { fn render_patterns(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area: Rect) {
use crate::model::PatternSpeed; use crate::model::PatternSpeed;
let theme = theme::get();
let is_focused = matches!(app.patterns_nav.column, PatternsColumn::Patterns); let is_focused = matches!(app.patterns_nav.column, PatternsColumn::Patterns);
let [title_area, inner] = let [title_area, inner] =
Layout::vertical([Constraint::Length(1), Constraint::Fill(1)]).areas(area); Layout::vertical([Constraint::Length(1), Constraint::Fill(1)]).areas(area);
let title_color = if is_focused { ui::HEADER } else { ui::UNFOCUSED }; let title_color = if is_focused { theme.ui.header } else { theme.ui.unfocused };
let bank = app.patterns_nav.bank_cursor; let bank = app.patterns_nav.bank_cursor;
let bank_name = app.project_state.project.banks[bank].name.as_deref(); let bank_name = app.project_state.project.banks[bank].name.as_deref();
@@ -249,13 +251,13 @@ fn render_patterns(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, a
let is_staged_stop = staged_to_stop.contains(&idx); let is_staged_stop = staged_to_stop.contains(&idx);
let (bg, fg, prefix) = match (is_cursor, is_playing, is_staged_play, is_staged_stop) { let (bg, fg, prefix) = match (is_cursor, is_playing, is_staged_play, is_staged_stop) {
(true, _, _, _) => (selection::CURSOR, selection::CURSOR_FG, ""), (true, _, _, _) => (theme.selection.cursor, theme.selection.cursor_fg, ""),
(false, true, _, true) => (list::STAGED_STOP_BG, list::STAGED_STOP_FG, "- "), (false, true, _, true) => (theme.list.staged_stop_bg, theme.list.staged_stop_fg, "- "),
(false, true, _, false) => (list::PLAYING_BG, list::PLAYING_FG, "> "), (false, true, _, false) => (theme.list.playing_bg, theme.list.playing_fg, "> "),
(false, false, true, _) => (list::STAGED_PLAY_BG, list::STAGED_PLAY_FG, "+ "), (false, false, true, _) => (theme.list.staged_play_bg, theme.list.staged_play_fg, "+ "),
(false, false, false, _) if is_selected => (list::HOVER_BG, list::HOVER_FG, ""), (false, false, false, _) if is_selected => (theme.list.hover_bg, theme.list.hover_fg, ""),
(false, false, false, _) if is_edit => (list::EDIT_BG, list::EDIT_FG, ""), (false, false, false, _) if is_edit => (theme.list.edit_bg, theme.list.edit_fg, ""),
(false, false, false, _) => (ui::BG, ui::TEXT_MUTED, ""), (false, false, false, _) => (theme.ui.bg, theme.ui.text_muted, ""),
}; };
let pattern = &app.project_state.project.banks[bank].patterns[idx]; let pattern = &app.project_state.project.banks[bank].patterns[idx];
@@ -314,7 +316,7 @@ fn render_patterns(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, a
} }
// Scroll indicators // Scroll indicators
let indicator_style = Style::new().fg(ui::TEXT_MUTED); let indicator_style = Style::new().fg(theme.ui.text_muted);
if scroll_offset > 0 { if scroll_offset > 0 {
let indicator = Paragraph::new("") let indicator = Paragraph::new("")
.style(indicator_style) .style(indicator_style)

View File

@@ -18,7 +18,7 @@ use crate::engine::{LinkState, SequencerSnapshot};
use crate::model::{SourceSpan, StepContext, Value}; use crate::model::{SourceSpan, StepContext, Value};
use crate::page::Page; use crate::page::Page;
use crate::state::{FlashKind, Modal, PanelFocus, PatternField, SidePanel, StackCache}; use crate::state::{FlashKind, Modal, PanelFocus, PatternField, SidePanel, StackCache};
use crate::theme::{browser, flash, header, hint, modal, search, status, table, ui}; use crate::theme;
use crate::views::highlight::{self, highlight_line, highlight_line_with_runtime}; use crate::views::highlight::{self, highlight_line, highlight_line_with_runtime};
use crate::widgets::{ use crate::widgets::{
ConfirmModal, ModalFrame, NavMinimap, NavTile, SampleBrowser, TextInputModal, ConfirmModal, ModalFrame, NavMinimap, NavTile, SampleBrowser, TextInputModal,
@@ -132,16 +132,17 @@ fn adjust_spans_for_line(
pub fn render(frame: &mut Frame, app: &App, link: &LinkState, snapshot: &SequencerSnapshot) { pub fn render(frame: &mut Frame, app: &App, link: &LinkState, snapshot: &SequencerSnapshot) {
let term = frame.area(); let term = frame.area();
let theme = theme::get();
let bg_color = if app.ui.event_flash > 0.0 { let bg_color = if app.ui.event_flash > 0.0 {
let t = (app.ui.event_flash * app.ui.flash_brightness).min(1.0); let t = (app.ui.event_flash * app.ui.flash_brightness).min(1.0);
let (base_r, base_g, base_b) = ui::BG_RGB; let (base_r, base_g, base_b) = theme.ui.bg_rgb;
let (tgt_r, tgt_g, tgt_b) = flash::EVENT_RGB; let (tgt_r, tgt_g, tgt_b) = theme.flash.event_rgb;
let r = base_r + ((tgt_r as f32 - base_r as f32) * t) as u8; let r = base_r + ((tgt_r as f32 - base_r as f32) * t) as u8;
let g = base_g + ((tgt_g as f32 - base_g as f32) * t) as u8; let g = base_g + ((tgt_g as f32 - base_g as f32) * t) as u8;
let b = base_b + ((tgt_b as f32 - base_b as f32) * t) as u8; let b = base_b + ((tgt_b as f32 - base_b as f32) * t) as u8;
Color::Rgb(r, g, b) Color::Rgb(r, g, b)
} else { } else {
ui::BG theme.ui.bg
}; };
let blank = " ".repeat(term.width as usize); let blank = " ".repeat(term.width as usize);
@@ -259,6 +260,7 @@ fn render_header(
) { ) {
use crate::model::PatternSpeed; use crate::model::PatternSpeed;
let theme = theme::get();
let bank = &app.project_state.project.banks[app.editor_ctx.bank]; let bank = &app.project_state.project.banks[app.editor_ctx.bank];
let pattern = &bank.patterns[app.editor_ctx.pattern]; let pattern = &bank.patterns[app.editor_ctx.pattern];
@@ -300,11 +302,11 @@ fn render_header(
// Transport block // Transport block
let (transport_bg, transport_text) = if app.playback.playing { let (transport_bg, transport_text) = if app.playback.playing {
(status::PLAYING_BG, " ▶ PLAYING ") (theme.status.playing_bg, " ▶ PLAYING ")
} else { } else {
(status::STOPPED_BG, " ■ STOPPED ") (theme.status.stopped_bg, " ■ STOPPED ")
}; };
let transport_style = Style::new().bg(transport_bg).fg(ui::TEXT_PRIMARY); let transport_style = Style::new().bg(transport_bg).fg(theme.ui.text_primary);
frame.render_widget( frame.render_widget(
Paragraph::new(transport_text) Paragraph::new(transport_text)
.style(transport_style) .style(transport_style)
@@ -314,8 +316,8 @@ fn render_header(
// Fill indicator // Fill indicator
let fill = app.live_keys.fill(); let fill = app.live_keys.fill();
let fill_fg = if fill { status::FILL_ON } else { status::FILL_OFF }; let fill_fg = if fill { theme.status.fill_on } else { theme.status.fill_off };
let fill_style = Style::new().bg(status::FILL_BG).fg(fill_fg); let fill_style = Style::new().bg(theme.status.fill_bg).fg(fill_fg);
frame.render_widget( frame.render_widget(
Paragraph::new(if fill { "F" } else { "·" }) Paragraph::new(if fill { "F" } else { "·" })
.style(fill_style) .style(fill_style)
@@ -325,8 +327,8 @@ fn render_header(
// Tempo block // Tempo block
let tempo_style = Style::new() let tempo_style = Style::new()
.bg(header::TEMPO_BG) .bg(theme.header.tempo_bg)
.fg(ui::TEXT_PRIMARY) .fg(theme.ui.text_primary)
.add_modifier(Modifier::BOLD); .add_modifier(Modifier::BOLD);
frame.render_widget( frame.render_widget(
Paragraph::new(format!(" {:.1} BPM ", link.tempo())) Paragraph::new(format!(" {:.1} BPM ", link.tempo()))
@@ -341,7 +343,7 @@ fn render_header(
.as_deref() .as_deref()
.map(|n| format!(" {n} ")) .map(|n| format!(" {n} "))
.unwrap_or_else(|| format!(" Bank {:02} ", app.editor_ctx.bank + 1)); .unwrap_or_else(|| format!(" Bank {:02} ", app.editor_ctx.bank + 1));
let bank_style = Style::new().bg(header::BANK_BG).fg(ui::TEXT_PRIMARY); let bank_style = Style::new().bg(theme.header.bank_bg).fg(theme.ui.text_primary);
frame.render_widget( frame.render_widget(
Paragraph::new(bank_name) Paragraph::new(bank_name)
.style(bank_style) .style(bank_style)
@@ -372,7 +374,7 @@ fn render_header(
" {} · {} steps{}{}{} ", " {} · {} steps{}{}{} ",
pattern_name, pattern.length, speed_info, page_info, iter_info pattern_name, pattern.length, speed_info, page_info, iter_info
); );
let pattern_style = Style::new().bg(header::PATTERN_BG).fg(ui::TEXT_PRIMARY); let pattern_style = Style::new().bg(theme.header.pattern_bg).fg(theme.ui.text_primary);
frame.render_widget( frame.render_widget(
Paragraph::new(pattern_text) Paragraph::new(pattern_text)
.style(pattern_style) .style(pattern_style)
@@ -385,7 +387,7 @@ fn render_header(
let peers = link.peers(); let peers = link.peers();
let voices = app.metrics.active_voices; let voices = app.metrics.active_voices;
let stats_text = format!(" CPU {cpu_pct:.0}% V:{voices} L:{peers} "); let stats_text = format!(" CPU {cpu_pct:.0}% V:{voices} L:{peers} ");
let stats_style = Style::new().bg(header::STATS_BG).fg(header::STATS_FG); let stats_style = Style::new().bg(theme.header.stats_bg).fg(theme.header.stats_fg);
frame.render_widget( frame.render_widget(
Paragraph::new(stats_text) Paragraph::new(stats_text)
.style(stats_style) .style(stats_style)
@@ -395,27 +397,30 @@ fn render_header(
} }
fn render_footer(frame: &mut Frame, app: &App, area: Rect) { fn render_footer(frame: &mut Frame, app: &App, area: Rect) {
let block = Block::default().borders(Borders::ALL); let theme = theme::get();
let block = Block::default()
.borders(Borders::ALL)
.border_style(Style::new().fg(theme.ui.border));
let inner = block.inner(area); let inner = block.inner(area);
let available_width = inner.width as usize; let available_width = inner.width as usize;
let page_indicator = match app.page { let page_indicator = match app.page {
Page::Main => "[MAIN]", Page::Main => " MAIN ",
Page::Patterns => "[PATTERNS]", Page::Patterns => " PATTERNS ",
Page::Engine => "[ENGINE]", Page::Engine => " ENGINE ",
Page::Options => "[OPTIONS]", Page::Options => " OPTIONS ",
Page::Help => "[HELP]", Page::Help => " HELP ",
Page::Dict => "[DICT]", Page::Dict => " DICT ",
}; };
let content = if let Some(ref msg) = app.ui.status_message { let content = if let Some(ref msg) = app.ui.status_message {
Line::from(vec![ Line::from(vec![
Span::styled( Span::styled(
page_indicator.to_string(), page_indicator.to_string(),
Style::new().fg(ui::TEXT_PRIMARY).add_modifier(Modifier::DIM), Style::new().bg(theme.view_badge.bg).fg(theme.view_badge.fg),
), ),
Span::raw(" "), Span::raw(" "),
Span::styled(msg.clone(), Style::new().fg(modal::CONFIRM)), Span::styled(msg.clone(), Style::new().fg(theme.modal.confirm)),
]) ])
} else { } else {
let bindings: Vec<(&str, &str)> = match app.page { let bindings: Vec<(&str, &str)> = match app.page {
@@ -477,7 +482,7 @@ fn render_footer(frame: &mut Frame, app: &App, area: Rect) {
let mut spans = vec![ let mut spans = vec![
Span::styled( Span::styled(
page_indicator.to_string(), page_indicator.to_string(),
Style::new().fg(ui::TEXT_PRIMARY).add_modifier(Modifier::DIM), Style::new().bg(theme.view_badge.bg).fg(theme.view_badge.fg),
), ),
Span::raw(" ".repeat(base_gap + if extra > 0 { 1 } else { 0 })), Span::raw(" ".repeat(base_gap + if extra > 0 { 1 } else { 0 })),
]; ];
@@ -485,11 +490,11 @@ fn render_footer(frame: &mut Frame, app: &App, area: Rect) {
for (i, (key, action)) in bindings.into_iter().enumerate() { for (i, (key, action)) in bindings.into_iter().enumerate() {
spans.push(Span::styled( spans.push(Span::styled(
key.to_string(), key.to_string(),
Style::new().fg(hint::KEY), Style::new().fg(theme.hint.key),
)); ));
spans.push(Span::styled( spans.push(Span::styled(
format!(": {action}"), format!(": {action}"),
Style::new().fg(hint::TEXT), Style::new().fg(theme.hint.text),
)); ));
if i < n - 1 { if i < n - 1 {
@@ -506,6 +511,7 @@ fn render_footer(frame: &mut Frame, app: &App, area: Rect) {
} }
fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term: Rect) { fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term: Rect) {
let theme = theme::get();
match &app.ui.modal { match &app.ui.modal {
Modal::None => {} Modal::None => {}
Modal::ConfirmQuit { selected } => { Modal::ConfirmQuit { selected } => {
@@ -539,8 +545,8 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
use crate::state::file_browser::FileBrowserMode; use crate::state::file_browser::FileBrowserMode;
use crate::widgets::FileBrowserModal; use crate::widgets::FileBrowserModal;
let (title, border_color) = match state.mode { let (title, border_color) = match state.mode {
FileBrowserMode::Save => ("Save As", flash::SUCCESS_FG), FileBrowserMode::Save => ("Save As", theme.flash.success_fg),
FileBrowserMode::Load => ("Load From", browser::DIRECTORY), FileBrowserMode::Load => ("Load From", theme.browser.directory),
}; };
let entries: Vec<(String, bool, bool)> = state let entries: Vec<(String, bool, bool)> = state
.entries .entries
@@ -558,7 +564,7 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
Modal::RenameBank { bank, name } => { Modal::RenameBank { bank, name } => {
TextInputModal::new(&format!("Rename Bank {:02}", bank + 1), name) TextInputModal::new(&format!("Rename Bank {:02}", bank + 1), name)
.width(40) .width(40)
.border_color(modal::RENAME) .border_color(theme.modal.rename)
.render_centered(frame, term); .render_centered(frame, term);
} }
Modal::RenamePattern { Modal::RenamePattern {
@@ -571,13 +577,13 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
name, name,
) )
.width(40) .width(40)
.border_color(modal::RENAME) .border_color(theme.modal.rename)
.render_centered(frame, term); .render_centered(frame, term);
} }
Modal::RenameStep { step, name, .. } => { Modal::RenameStep { step, name, .. } => {
TextInputModal::new(&format!("Name Step {:02}", step + 1), name) TextInputModal::new(&format!("Name Step {:02}", step + 1), name)
.width(40) .width(40)
.border_color(modal::INPUT) .border_color(theme.modal.input)
.render_centered(frame, term); .render_centered(frame, term);
} }
Modal::SetPattern { field, input } => { Modal::SetPattern { field, input } => {
@@ -588,14 +594,14 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
TextInputModal::new(title, input) TextInputModal::new(title, input)
.hint(hint) .hint(hint)
.width(45) .width(45)
.border_color(modal::CONFIRM) .border_color(theme.modal.confirm)
.render_centered(frame, term); .render_centered(frame, term);
} }
Modal::SetTempo(input) => { Modal::SetTempo(input) => {
TextInputModal::new("Set Tempo (20-300 BPM)", input) TextInputModal::new("Set Tempo (20-300 BPM)", input)
.hint("Enter BPM") .hint("Enter BPM")
.width(30) .width(30)
.border_color(modal::RENAME) .border_color(theme.modal.rename)
.render_centered(frame, term); .render_centered(frame, term);
} }
Modal::AddSamplePath(state) => { Modal::AddSamplePath(state) => {
@@ -608,7 +614,7 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
FileBrowserModal::new("Add Sample Path", &state.input, &entries) FileBrowserModal::new("Add Sample Path", &state.input, &entries)
.selected(state.selected) .selected(state.selected)
.scroll_offset(state.scroll_offset) .scroll_offset(state.scroll_offset)
.border_color(modal::RENAME) .border_color(theme.modal.rename)
.width(60) .width(60)
.height(18) .height(18)
.render_centered(frame, term); .render_centered(frame, term);
@@ -633,14 +639,14 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
let inner = ModalFrame::new(&title) let inner = ModalFrame::new(&title)
.width(width) .width(width)
.height(height) .height(height)
.border_color(modal::PREVIEW) .border_color(theme.modal.preview)
.render_centered(frame, term); .render_centered(frame, term);
let script = pattern.resolve_script(step_idx).unwrap_or(""); let script = pattern.resolve_script(step_idx).unwrap_or("");
if script.is_empty() { if script.is_empty() {
let empty = Paragraph::new("(empty)") let empty = Paragraph::new("(empty)")
.alignment(Alignment::Center) .alignment(Alignment::Center)
.style(Style::new().fg(ui::TEXT_DIM)); .style(Style::new().fg(theme.ui.text_dim));
let centered_area = Rect { let centered_area = Rect {
y: inner.y + inner.height / 2, y: inner.y + inner.height / 2,
height: 1, height: 1,
@@ -695,10 +701,10 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
let flash_kind = app.ui.flash_kind(); let flash_kind = app.ui.flash_kind();
let border_color = match flash_kind { let border_color = match flash_kind {
Some(FlashKind::Error) => flash::ERROR_FG, Some(FlashKind::Error) => theme.flash.error_fg,
Some(FlashKind::Info) => ui::TEXT_PRIMARY, Some(FlashKind::Info) => theme.ui.text_primary,
Some(FlashKind::Success) => flash::SUCCESS_FG, Some(FlashKind::Success) => theme.flash.success_fg,
None => modal::EDITOR, None => theme.modal.editor,
}; };
let title = if let Some(ref name) = step.and_then(|s| s.name.as_ref()) { let title = if let Some(ref name) = step.and_then(|s| s.name.as_ref()) {
@@ -765,9 +771,9 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
if let Some(sa) = search_area { if let Some(sa) = search_area {
let style = if app.editor_ctx.editor.search_active() { let style = if app.editor_ctx.editor.search_active() {
Style::default().fg(search::ACTIVE) Style::default().fg(theme.search.active)
} else { } else {
Style::default().fg(search::INACTIVE) Style::default().fg(theme.search.inactive)
}; };
let cursor = if app.editor_ctx.editor.search_active() { let cursor = if app.editor_ctx.editor.search_active() {
"_" "_"
@@ -780,9 +786,9 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
if let Some(kind) = flash_kind { if let Some(kind) = flash_kind {
let bg = match kind { let bg = match kind {
FlashKind::Error => flash::ERROR_BG, FlashKind::Error => theme.flash.error_bg,
FlashKind::Info => flash::INFO_BG, FlashKind::Info => theme.flash.info_bg,
FlashKind::Success => flash::SUCCESS_BG, FlashKind::Success => theme.flash.success_bg,
}; };
let flash_block = Block::default().style(Style::default().bg(bg)); let flash_block = Block::default().style(Style::default().bg(bg));
frame.render_widget(flash_block, editor_area); frame.render_widget(flash_block, editor_area);
@@ -791,8 +797,8 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
.editor .editor
.render(frame, editor_area, &highlighter); .render(frame, editor_area, &highlighter);
let dim = Style::default().fg(hint::TEXT); let dim = Style::default().fg(theme.hint.text);
let key = Style::default().fg(hint::KEY); let key = Style::default().fg(theme.hint.key);
if app.editor_ctx.editor.search_active() { if app.editor_ctx.editor.search_active() {
let hint = Line::from(vec![ let hint = Line::from(vec![
@@ -860,7 +866,7 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
let block = Block::bordered() let block = Block::bordered()
.title(format!(" Pattern B{:02}:P{:02} ", bank + 1, pattern + 1)) .title(format!(" Pattern B{:02}:P{:02} ", bank + 1, pattern + 1))
.border_style(Style::default().fg(modal::INPUT)); .border_style(Style::default().fg(theme.modal.input));
let inner = block.inner(area); let inner = block.inner(area);
frame.render_widget(Clear, area); frame.render_widget(Clear, area);
@@ -895,14 +901,14 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
let (label_style, value_style) = if *selected { let (label_style, value_style) = if *selected {
( (
Style::default() Style::default()
.fg(hint::KEY) .fg(theme.hint.key)
.add_modifier(Modifier::BOLD), .add_modifier(Modifier::BOLD),
Style::default().fg(ui::TEXT_PRIMARY).bg(ui::SURFACE), Style::default().fg(theme.ui.text_primary).bg(theme.ui.surface),
) )
} else { } else {
( (
Style::default().fg(ui::TEXT_MUTED), Style::default().fg(theme.ui.text_muted),
Style::default().fg(ui::TEXT_PRIMARY), Style::default().fg(theme.ui.text_primary),
) )
}; };
@@ -918,14 +924,14 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
let hint_area = Rect::new(inner.x, inner.y + inner.height - 1, inner.width, 1); let hint_area = Rect::new(inner.x, inner.y + inner.height - 1, inner.width, 1);
let hint_line = Line::from(vec![ let hint_line = Line::from(vec![
Span::styled("↑↓", Style::default().fg(hint::KEY)), Span::styled("↑↓", Style::default().fg(theme.hint.key)),
Span::styled(" nav ", Style::default().fg(hint::TEXT)), Span::styled(" nav ", Style::default().fg(theme.hint.text)),
Span::styled("←→", Style::default().fg(hint::KEY)), Span::styled("←→", Style::default().fg(theme.hint.key)),
Span::styled(" change ", Style::default().fg(hint::TEXT)), Span::styled(" change ", Style::default().fg(theme.hint.text)),
Span::styled("Enter", Style::default().fg(hint::KEY)), Span::styled("Enter", Style::default().fg(theme.hint.key)),
Span::styled(" save ", Style::default().fg(hint::TEXT)), Span::styled(" save ", Style::default().fg(theme.hint.text)),
Span::styled("Esc", Style::default().fg(hint::KEY)), Span::styled("Esc", Style::default().fg(theme.hint.key)),
Span::styled(" cancel", Style::default().fg(hint::TEXT)), Span::styled(" cancel", Style::default().fg(theme.hint.text)),
]); ]);
frame.render_widget(Paragraph::new(hint_line), hint_area); frame.render_widget(Paragraph::new(hint_line), hint_area);
} }
@@ -937,7 +943,7 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
let inner = ModalFrame::new(&title) let inner = ModalFrame::new(&title)
.width(width) .width(width)
.height(height) .height(height)
.border_color(modal::EDITOR) .border_color(theme.modal.editor)
.render_centered(frame, term); .render_centered(frame, term);
let bindings = super::keybindings::bindings_for(app.page); let bindings = super::keybindings::bindings_for(app.page);
@@ -949,11 +955,11 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
.skip(*scroll) .skip(*scroll)
.take(visible_rows) .take(visible_rows)
.map(|(i, (key, name, desc))| { .map(|(i, (key, name, desc))| {
let bg = if i % 2 == 0 { table::ROW_EVEN } else { table::ROW_ODD }; let bg = if i % 2 == 0 { theme.table.row_even } else { theme.table.row_odd };
Row::new(vec![ Row::new(vec![
Cell::from(*key).style(Style::default().fg(modal::CONFIRM)), Cell::from(*key).style(Style::default().fg(theme.modal.confirm)),
Cell::from(*name).style(Style::default().fg(modal::INPUT)), Cell::from(*name).style(Style::default().fg(theme.modal.input)),
Cell::from(*desc).style(Style::default().fg(ui::TEXT_PRIMARY)), Cell::from(*desc).style(Style::default().fg(theme.ui.text_primary)),
]) ])
.style(Style::default().bg(bg)) .style(Style::default().bg(bg))
}) })
@@ -984,12 +990,12 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
height: 1, height: 1,
}; };
let keybind_hint = Line::from(vec![ let keybind_hint = Line::from(vec![
Span::styled("↑↓", Style::default().fg(hint::KEY)), Span::styled("↑↓", Style::default().fg(theme.hint.key)),
Span::styled(" scroll ", Style::default().fg(hint::TEXT)), Span::styled(" scroll ", Style::default().fg(theme.hint.text)),
Span::styled("PgUp/Dn", Style::default().fg(hint::KEY)), Span::styled("PgUp/Dn", Style::default().fg(theme.hint.key)),
Span::styled(" page ", Style::default().fg(hint::TEXT)), Span::styled(" page ", Style::default().fg(theme.hint.text)),
Span::styled("Esc/?", Style::default().fg(hint::KEY)), Span::styled("Esc/?", Style::default().fg(theme.hint.key)),
Span::styled(" close", Style::default().fg(hint::TEXT)), Span::styled(" close", Style::default().fg(theme.hint.text)),
]); ]);
frame.render_widget(Paragraph::new(keybind_hint).alignment(Alignment::Right), hint_area); frame.render_widget(Paragraph::new(keybind_hint).alignment(Alignment::Right), hint_area);
} }

View File

@@ -6,18 +6,19 @@ use ratatui::Frame;
use tui_big_text::{BigText, PixelSize}; use tui_big_text::{BigText, PixelSize};
use crate::state::ui::UiState; use crate::state::ui::UiState;
use crate::theme::title; use crate::theme;
pub fn render(frame: &mut Frame, area: Rect, ui: &UiState) { pub fn render(frame: &mut Frame, area: Rect, ui: &UiState) {
let theme = theme::get();
frame.render_widget(&ui.sparkles, area); frame.render_widget(&ui.sparkles, area);
let author_style = Style::new().fg(title::AUTHOR); let author_style = Style::new().fg(theme.title.author);
let link_style = Style::new().fg(title::LINK); let link_style = Style::new().fg(theme.title.link);
let license_style = Style::new().fg(title::LICENSE); let license_style = Style::new().fg(theme.title.license);
let big_title = BigText::builder() let big_title = BigText::builder()
.pixel_size(PixelSize::Quadrant) .pixel_size(PixelSize::Quadrant)
.style(Style::new().fg(title::BIG_TITLE).bold()) .style(Style::new().fg(theme.title.big_title).bold())
.lines(vec!["CAGIRE".into()]) .lines(vec!["CAGIRE".into()])
.centered() .centered()
.build(); .build();
@@ -26,7 +27,7 @@ pub fn render(frame: &mut Frame, area: Rect, ui: &UiState) {
Line::from(""), Line::from(""),
Line::from(Span::styled( Line::from(Span::styled(
"A Forth Music Sequencer", "A Forth Music Sequencer",
Style::new().fg(title::SUBTITLE), Style::new().fg(theme.title.subtitle),
)), )),
Line::from(""), Line::from(""),
Line::from(Span::styled("by BuboBubo", author_style)), Line::from(Span::styled("by BuboBubo", author_style)),
@@ -38,7 +39,7 @@ pub fn render(frame: &mut Frame, area: Rect, ui: &UiState) {
Line::from(""), Line::from(""),
Line::from(Span::styled( Line::from(Span::styled(
"Press any key to continue", "Press any key to continue",
Style::new().fg(title::PROMPT), Style::new().fg(theme.title.prompt),
)), )),
]; ];