Compare commits
4 Commits
5fb059ea20
...
7658cf9d51
| Author | SHA1 | Date | |
|---|---|---|---|
| 7658cf9d51 | |||
| 584dbb6aad | |||
| 2731eea037 | |||
| 22ee5f97e6 |
16
.github/workflows/ci.yml
vendored
16
.github/workflows/ci.yml
vendored
@@ -1,8 +1,8 @@
|
|||||||
name: CI
|
name: CI
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
|
||||||
tags: ['v*']
|
tags: ['v*']
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
@@ -105,8 +105,20 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
path: artifacts
|
path: artifacts
|
||||||
|
|
||||||
|
- name: Prepare release files
|
||||||
|
run: |
|
||||||
|
mkdir -p release
|
||||||
|
for dir in artifacts/*/; do
|
||||||
|
name=$(basename "$dir")
|
||||||
|
if [ -f "$dir/cagire.exe" ]; then
|
||||||
|
cp "$dir/cagire.exe" "release/${name}.exe"
|
||||||
|
elif [ -f "$dir/cagire" ]; then
|
||||||
|
cp "$dir/cagire" "release/${name}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
- name: Create Release
|
- name: Create Release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
files: artifacts/**/*
|
files: release/*
|
||||||
generate_release_notes: true
|
generate_release_notes: true
|
||||||
|
|||||||
3
.github/workflows/pages.yml
vendored
3
.github/workflows/pages.yml
vendored
@@ -3,9 +3,6 @@ name: Deploy Website
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
paths:
|
|
||||||
- 'website/**'
|
|
||||||
- '.github/workflows/pages.yml'
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
|
|||||||
28
Cargo.toml
28
Cargo.toml
@@ -14,20 +14,35 @@ path = "src/lib.rs"
|
|||||||
name = "cagire"
|
name = "cagire"
|
||||||
path = "src/main.rs"
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "cagire-desktop"
|
||||||
|
path = "src/bin/desktop.rs"
|
||||||
|
required-features = ["desktop"]
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = []
|
||||||
|
desktop = [
|
||||||
|
"egui",
|
||||||
|
"eframe",
|
||||||
|
"egui_ratatui",
|
||||||
|
"soft_ratatui",
|
||||||
|
"image",
|
||||||
|
]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
cagire-forth = { path = "crates/forth" }
|
cagire-forth = { path = "crates/forth" }
|
||||||
cagire-project = { path = "crates/project" }
|
cagire-project = { path = "crates/project" }
|
||||||
cagire-ratatui = { path = "crates/ratatui" }
|
cagire-ratatui = { path = "crates/ratatui" }
|
||||||
doux = { git = "https://github.com/sova-org/doux", features = ["native"] }
|
doux = { git = "https://github.com/sova-org/doux", features = ["native"] }
|
||||||
rusty_link = "0.4"
|
rusty_link = "0.4"
|
||||||
ratatui = "0.29"
|
ratatui = "0.30"
|
||||||
crossterm = "0.28"
|
crossterm = "0.29"
|
||||||
cpal = "0.15"
|
cpal = "0.15"
|
||||||
clap = { version = "4", features = ["derive"] }
|
clap = { version = "4", features = ["derive"] }
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
tui-big-text = "0.7"
|
tui-big-text = "0.8"
|
||||||
arboard = "3"
|
arboard = "3"
|
||||||
minimad = "0.13"
|
minimad = "0.13"
|
||||||
crossbeam-channel = "0.5"
|
crossbeam-channel = "0.5"
|
||||||
@@ -37,6 +52,13 @@ thread-priority = "1"
|
|||||||
ringbuf = "0.4"
|
ringbuf = "0.4"
|
||||||
arc-swap = "1"
|
arc-swap = "1"
|
||||||
|
|
||||||
|
# Desktop-only dependencies (behind feature flag)
|
||||||
|
egui = { version = "0.33", optional = true }
|
||||||
|
eframe = { version = "0.33", optional = true }
|
||||||
|
egui_ratatui = { version = "2.1", optional = true }
|
||||||
|
soft_ratatui = { version = "0.1.3", features = ["unicodefonts"], optional = true }
|
||||||
|
image = { version = "0.25", default-features = false, features = ["png"], optional = true }
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
opt-level = 3
|
opt-level = 3
|
||||||
lto = "fat"
|
lto = "fat"
|
||||||
|
|||||||
16
README.md
16
README.md
@@ -1,19 +1,35 @@
|
|||||||
|
<p align="center">
|
||||||
|
<img src="cagire_pixel.png" alt="Cagire" width="256">
|
||||||
|
</p>
|
||||||
|
|
||||||
# Cagire
|
# Cagire
|
||||||
|
|
||||||
A Forth Music Sequencer.
|
A Forth Music Sequencer.
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
|
Terminal version:
|
||||||
```
|
```
|
||||||
cargo build --release
|
cargo build --release
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Desktop version (with egui window):
|
||||||
|
```
|
||||||
|
cargo build --release --features desktop --bin cagire-desktop
|
||||||
|
```
|
||||||
|
|
||||||
## Run
|
## Run
|
||||||
|
|
||||||
|
Terminal version:
|
||||||
```
|
```
|
||||||
cargo run --release
|
cargo run --release
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Desktop version:
|
||||||
|
```
|
||||||
|
cargo run --release --features desktop --bin cagire-desktop
|
||||||
|
```
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
AGPL-3.0
|
AGPL-3.0
|
||||||
|
|||||||
BIN
cagire_pixel.png
Normal file
BIN
cagire_pixel.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
@@ -5,6 +5,6 @@ edition = "2021"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
ratatui = "0.29"
|
ratatui = "0.30"
|
||||||
regex = "1"
|
regex = "1"
|
||||||
tui-textarea = { version = "0.7", features = ["search"] }
|
tui-textarea = { git = "https://github.com/phsym/tui-textarea", branch = "main", features = ["search"] }
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
|
use crate::theme;
|
||||||
use ratatui::layout::{Alignment, Constraint, Layout, Rect};
|
use ratatui::layout::{Alignment, Constraint, Layout, Rect};
|
||||||
use ratatui::style::{Color, Style};
|
use ratatui::style::Style;
|
||||||
use ratatui::text::{Line, Span};
|
use ratatui::text::{Line, Span};
|
||||||
use ratatui::widgets::Paragraph;
|
use ratatui::widgets::Paragraph;
|
||||||
use ratatui::Frame;
|
use ratatui::Frame;
|
||||||
@@ -22,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(Color::Yellow)
|
.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);
|
||||||
@@ -36,12 +38,12 @@ impl<'a> ConfirmModal<'a> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let yes_style = if self.selected {
|
let yes_style = if self.selected {
|
||||||
Style::new().fg(Color::Black).bg(Color::Yellow)
|
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(Color::Black).bg(Color::Yellow)
|
Style::new().fg(t.confirm.button_selected_fg).bg(t.confirm.button_selected_bg)
|
||||||
} else {
|
} else {
|
||||||
Style::default()
|
Style::default()
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
use crate::theme;
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
layout::Rect,
|
layout::Rect,
|
||||||
style::{Color, Modifier, Style},
|
style::{Modifier, Style},
|
||||||
text::{Line, Span},
|
text::{Line, Span},
|
||||||
widgets::{Clear, Paragraph},
|
widgets::{Clear, Paragraph},
|
||||||
Frame,
|
Frame,
|
||||||
@@ -332,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(Color::White).fg(Color::Black);
|
let cursor_style = Style::default().bg(t.editor_widget.cursor_bg).fg(t.editor_widget.cursor_fg);
|
||||||
let selection_style = Style::default().bg(Color::Rgb(60, 80, 120));
|
let selection_style = Style::default().bg(t.editor_widget.selection_bg);
|
||||||
|
|
||||||
let selection = self.text.selection_range();
|
let selection = self.text.selection_range();
|
||||||
|
|
||||||
@@ -382,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;
|
||||||
@@ -412,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(Color::Yellow).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(Color::White);
|
let normal_style = Style::default().fg(t.editor_widget.completion_fg);
|
||||||
let bg_style = Style::default().bg(Color::Rgb(30, 30, 40));
|
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| {
|
||||||
@@ -427,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(Color::Rgb(30, 30, 40))))
|
Line::from(Span::styled(display, style.bg(t.editor_widget.completion_bg)))
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
@@ -450,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(Color::Yellow)
|
.fg(t.editor_widget.completion_selected)
|
||||||
.add_modifier(Modifier::BOLD)
|
.add_modifier(Modifier::BOLD)
|
||||||
.bg(Color::Rgb(30, 30, 40));
|
.bg(t.editor_widget.completion_bg);
|
||||||
let desc_style = Style::default()
|
let desc_style = Style::default()
|
||||||
.fg(Color::White)
|
.fg(t.editor_widget.completion_fg)
|
||||||
.bg(Color::Rgb(30, 30, 40));
|
.bg(t.editor_widget.completion_bg);
|
||||||
let example_style = Style::default()
|
let example_style = Style::default()
|
||||||
.fg(Color::Rgb(120, 200, 160))
|
.fg(t.editor_widget.completion_example)
|
||||||
.bg(Color::Rgb(30, 30, 40));
|
.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();
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
use crate::theme::{browser, input, ui};
|
||||||
|
use ratatui::style::Color;
|
||||||
use ratatui::layout::{Constraint, Layout, Rect};
|
use ratatui::layout::{Constraint, Layout, Rect};
|
||||||
use ratatui::style::{Color, Style};
|
use ratatui::style::Style;
|
||||||
use ratatui::text::{Line, Span};
|
use ratatui::text::{Line, Span};
|
||||||
use ratatui::widgets::Paragraph;
|
use ratatui::widgets::Paragraph;
|
||||||
use ratatui::Frame;
|
use ratatui::Frame;
|
||||||
@@ -25,7 +27,7 @@ impl<'a> FileBrowserModal<'a> {
|
|||||||
entries,
|
entries,
|
||||||
selected: 0,
|
selected: 0,
|
||||||
scroll_offset: 0,
|
scroll_offset: 0,
|
||||||
border_color: Color::White,
|
border_color: ui::TEXT_PRIMARY,
|
||||||
width: 60,
|
width: 60,
|
||||||
height: 16,
|
height: 16,
|
||||||
}
|
}
|
||||||
@@ -69,8 +71,8 @@ impl<'a> FileBrowserModal<'a> {
|
|||||||
frame.render_widget(
|
frame.render_widget(
|
||||||
Paragraph::new(Line::from(vec![
|
Paragraph::new(Line::from(vec![
|
||||||
Span::raw("> "),
|
Span::raw("> "),
|
||||||
Span::styled(self.input, Style::new().fg(Color::Cyan)),
|
Span::styled(self.input, Style::new().fg(input::TEXT)),
|
||||||
Span::styled("█", Style::new().fg(Color::White)),
|
Span::styled("█", Style::new().fg(input::CURSOR)),
|
||||||
])),
|
])),
|
||||||
rows[0],
|
rows[0],
|
||||||
);
|
);
|
||||||
@@ -95,13 +97,13 @@ impl<'a> FileBrowserModal<'a> {
|
|||||||
format!("{prefix}{name}")
|
format!("{prefix}{name}")
|
||||||
};
|
};
|
||||||
let color = if is_selected {
|
let color = if is_selected {
|
||||||
Color::Yellow
|
browser::SELECTED
|
||||||
} else if *is_dir {
|
} else if *is_dir {
|
||||||
Color::Blue
|
browser::DIRECTORY
|
||||||
} else if *is_cagire {
|
} else if *is_cagire {
|
||||||
Color::Magenta
|
browser::PROJECT_FILE
|
||||||
} else {
|
} else {
|
||||||
Color::White
|
browser::FILE
|
||||||
};
|
};
|
||||||
Line::from(Span::styled(display, Style::new().fg(color)))
|
Line::from(Span::styled(display, Style::new().fg(color)))
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ mod scope;
|
|||||||
mod sparkles;
|
mod sparkles;
|
||||||
mod spectrum;
|
mod spectrum;
|
||||||
mod text_input;
|
mod text_input;
|
||||||
|
pub mod theme;
|
||||||
mod vu_meter;
|
mod vu_meter;
|
||||||
|
|
||||||
pub use confirm::ConfirmModal;
|
pub use confirm::ConfirmModal;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
|
use crate::theme::{hint, ui};
|
||||||
use ratatui::layout::Rect;
|
use ratatui::layout::Rect;
|
||||||
use ratatui::style::{Color, Modifier, Style};
|
use ratatui::style::{Modifier, Style};
|
||||||
use ratatui::text::{Line, Span};
|
use ratatui::text::{Line, Span};
|
||||||
use ratatui::widgets::Paragraph;
|
use ratatui::widgets::Paragraph;
|
||||||
use ratatui::Frame;
|
use ratatui::Frame;
|
||||||
@@ -50,10 +51,10 @@ impl<'a> ListSelect<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn render(self, frame: &mut Frame, area: Rect) {
|
pub fn render(self, frame: &mut Frame, area: Rect) {
|
||||||
let cursor_style = Style::new().fg(Color::Yellow).add_modifier(Modifier::BOLD);
|
let cursor_style = Style::new().fg(hint::KEY).add_modifier(Modifier::BOLD);
|
||||||
let selected_style = Style::new().fg(Color::Cyan);
|
let selected_style = Style::new().fg(ui::ACCENT);
|
||||||
let normal_style = Style::default();
|
let normal_style = Style::default();
|
||||||
let indicator_style = Style::new().fg(Color::DarkGray);
|
let indicator_style = Style::new().fg(ui::TEXT_DIM);
|
||||||
|
|
||||||
let visible_end = (self.scroll_offset + self.visible_count).min(self.items.len());
|
let visible_end = (self.scroll_offset + self.visible_count).min(self.items.len());
|
||||||
let has_above = self.scroll_offset > 0;
|
let has_above = self.scroll_offset > 0;
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
|
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};
|
use ratatui::widgets::{Block, Borders, Clear, Paragraph};
|
||||||
use ratatui::Frame;
|
use ratatui::Frame;
|
||||||
|
|
||||||
pub struct ModalFrame<'a> {
|
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> {
|
||||||
@@ -16,7 +17,7 @@ impl<'a> ModalFrame<'a> {
|
|||||||
title,
|
title,
|
||||||
width: 40,
|
width: 40,
|
||||||
height: 5,
|
height: 5,
|
||||||
border_color: Color::White,
|
border_color: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,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));
|
||||||
|
|
||||||
@@ -45,10 +47,21 @@ impl<'a> ModalFrame<'a> {
|
|||||||
|
|
||||||
frame.render_widget(Clear, area);
|
frame.render_widget(Clear, area);
|
||||||
|
|
||||||
|
// Fill background with theme color
|
||||||
|
let bg_fill = " ".repeat(area.width as usize);
|
||||||
|
for row in 0..area.height {
|
||||||
|
let line_area = Rect::new(area.x, area.y + row, area.width, 1);
|
||||||
|
frame.render_widget(
|
||||||
|
Paragraph::new(bg_fill.clone()).style(Style::new().bg(t.ui.bg)),
|
||||||
|
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);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
|
use crate::theme;
|
||||||
use ratatui::layout::{Alignment, Rect};
|
use ratatui::layout::{Alignment, Rect};
|
||||||
use ratatui::style::{Color, Style};
|
use ratatui::style::Style;
|
||||||
use ratatui::widgets::{Clear, Paragraph};
|
use ratatui::widgets::{Clear, Paragraph};
|
||||||
use ratatui::Frame;
|
use ratatui::Frame;
|
||||||
|
|
||||||
@@ -48,6 +49,17 @@ impl<'a> NavMinimap<'a> {
|
|||||||
|
|
||||||
frame.render_widget(Clear, area);
|
frame.render_widget(Clear, area);
|
||||||
|
|
||||||
|
// Fill background with theme color
|
||||||
|
let t = theme::get();
|
||||||
|
let bg_fill = " ".repeat(area.width as usize);
|
||||||
|
for row in 0..area.height {
|
||||||
|
let line_area = Rect::new(area.x, area.y + row, area.width, 1);
|
||||||
|
frame.render_widget(
|
||||||
|
Paragraph::new(bg_fill.clone()).style(Style::new().bg(t.ui.bg)),
|
||||||
|
line_area,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let inner_x = area.x + pad;
|
let inner_x = area.x + pad;
|
||||||
let inner_y = area.y + pad;
|
let inner_y = area.y + pad;
|
||||||
|
|
||||||
@@ -61,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 {
|
||||||
(Color::Rgb(50, 90, 110), Color::White)
|
(t.nav.selected_bg, t.nav.selected_fg)
|
||||||
} else {
|
} else {
|
||||||
(Color::Rgb(30, 35, 45), Color::Rgb(100, 105, 115))
|
(t.nav.unselected_bg, t.nav.unselected_fg)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Fill background
|
// Fill background
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
|
use crate::theme::{browser, search};
|
||||||
use ratatui::layout::{Constraint, Layout, Rect};
|
use ratatui::layout::{Constraint, Layout, Rect};
|
||||||
use ratatui::style::{Color, Modifier, Style};
|
use ratatui::style::{Modifier, Style};
|
||||||
use ratatui::text::{Line, Span};
|
use ratatui::text::{Line, Span};
|
||||||
use ratatui::widgets::{Block, Borders, Paragraph};
|
use ratatui::widgets::{Block, Borders, Paragraph};
|
||||||
use ratatui::Frame;
|
use ratatui::Frame;
|
||||||
@@ -59,9 +60,9 @@ impl<'a> SampleBrowser<'a> {
|
|||||||
|
|
||||||
pub fn render(self, frame: &mut Frame, area: Rect) {
|
pub fn render(self, frame: &mut Frame, area: Rect) {
|
||||||
let border_style = if self.focused {
|
let border_style = if self.focused {
|
||||||
Style::new().fg(Color::Yellow)
|
Style::new().fg(browser::FOCUSED_BORDER)
|
||||||
} else {
|
} else {
|
||||||
Style::new().fg(Color::DarkGray)
|
Style::new().fg(browser::UNFOCUSED_BORDER)
|
||||||
};
|
};
|
||||||
|
|
||||||
let block = Block::default()
|
let block = Block::default()
|
||||||
@@ -96,9 +97,9 @@ impl<'a> SampleBrowser<'a> {
|
|||||||
|
|
||||||
fn render_search(&self, frame: &mut Frame, area: Rect) {
|
fn render_search(&self, frame: &mut Frame, area: Rect) {
|
||||||
let style = if self.search_active {
|
let style = if self.search_active {
|
||||||
Style::new().fg(Color::Yellow)
|
Style::new().fg(search::ACTIVE)
|
||||||
} else {
|
} else {
|
||||||
Style::new().fg(Color::DarkGray)
|
Style::new().fg(search::INACTIVE)
|
||||||
};
|
};
|
||||||
let cursor = if self.search_active { "_" } else { "" };
|
let cursor = if self.search_active { "_" } else { "" };
|
||||||
let text = format!("/{}{}", self.search_query, cursor);
|
let text = format!("/{}{}", self.search_query, cursor);
|
||||||
@@ -114,7 +115,7 @@ impl<'a> SampleBrowser<'a> {
|
|||||||
} else {
|
} else {
|
||||||
"No matches"
|
"No matches"
|
||||||
};
|
};
|
||||||
let line = Line::from(Span::styled(msg, Style::new().fg(Color::DarkGray)));
|
let line = Line::from(Span::styled(msg, Style::new().fg(browser::EMPTY_TEXT)));
|
||||||
frame.render_widget(Paragraph::new(vec![line]), area);
|
frame.render_widget(Paragraph::new(vec![line]), area);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -129,23 +130,23 @@ impl<'a> SampleBrowser<'a> {
|
|||||||
|
|
||||||
let (icon, icon_color) = match entry.kind {
|
let (icon, icon_color) = match entry.kind {
|
||||||
TreeLineKind::Root { expanded: true } | TreeLineKind::Folder { expanded: true } => {
|
TreeLineKind::Root { expanded: true } | TreeLineKind::Folder { expanded: true } => {
|
||||||
("\u{25BC} ", Color::Cyan)
|
("\u{25BC} ", browser::FOLDER_ICON)
|
||||||
}
|
}
|
||||||
TreeLineKind::Root { expanded: false }
|
TreeLineKind::Root { expanded: false }
|
||||||
| TreeLineKind::Folder { expanded: false } => ("\u{25B6} ", Color::Cyan),
|
| TreeLineKind::Folder { expanded: false } => ("\u{25B6} ", browser::FOLDER_ICON),
|
||||||
TreeLineKind::File => ("\u{266A} ", Color::DarkGray),
|
TreeLineKind::File => ("\u{266A} ", browser::FILE_ICON),
|
||||||
};
|
};
|
||||||
|
|
||||||
let label_style = if is_cursor && self.focused {
|
let label_style = if is_cursor && self.focused {
|
||||||
Style::new().fg(Color::Yellow).add_modifier(Modifier::BOLD)
|
Style::new().fg(browser::SELECTED).add_modifier(Modifier::BOLD)
|
||||||
} else if is_cursor {
|
} else if is_cursor {
|
||||||
Style::new().fg(Color::White)
|
Style::new().fg(browser::FILE)
|
||||||
} else {
|
} else {
|
||||||
match entry.kind {
|
match entry.kind {
|
||||||
TreeLineKind::Root { .. } => {
|
TreeLineKind::Root { .. } => {
|
||||||
Style::new().fg(Color::White).add_modifier(Modifier::BOLD)
|
Style::new().fg(browser::ROOT).add_modifier(Modifier::BOLD)
|
||||||
}
|
}
|
||||||
TreeLineKind::Folder { .. } => Style::new().fg(Color::Cyan),
|
TreeLineKind::Folder { .. } => Style::new().fg(browser::DIRECTORY),
|
||||||
TreeLineKind::File => Style::default(),
|
TreeLineKind::File => Style::default(),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use crate::theme::meter;
|
||||||
use ratatui::buffer::Buffer;
|
use ratatui::buffer::Buffer;
|
||||||
use ratatui::layout::Rect;
|
use ratatui::layout::Rect;
|
||||||
use ratatui::style::Color;
|
use ratatui::style::Color;
|
||||||
@@ -26,7 +27,7 @@ impl<'a> Scope<'a> {
|
|||||||
Self {
|
Self {
|
||||||
data,
|
data,
|
||||||
orientation: Orientation::Horizontal,
|
orientation: Orientation::Horizontal,
|
||||||
color: Color::Green,
|
color: meter::LOW,
|
||||||
gain: 1.0,
|
gain: 1.0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use crate::theme::sparkle;
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
use ratatui::buffer::Buffer;
|
use ratatui::buffer::Buffer;
|
||||||
use ratatui::layout::Rect;
|
use ratatui::layout::Rect;
|
||||||
@@ -5,13 +6,6 @@ use ratatui::style::{Color, Style};
|
|||||||
use ratatui::widgets::Widget;
|
use ratatui::widgets::Widget;
|
||||||
|
|
||||||
const CHARS: &[char] = &['·', '✦', '✧', '°', '•', '+', '⋆', '*'];
|
const CHARS: &[char] = &['·', '✦', '✧', '°', '•', '+', '⋆', '*'];
|
||||||
const COLORS: &[(u8, u8, u8)] = &[
|
|
||||||
(200, 220, 255),
|
|
||||||
(255, 200, 150),
|
|
||||||
(150, 255, 200),
|
|
||||||
(255, 150, 200),
|
|
||||||
(200, 150, 255),
|
|
||||||
];
|
|
||||||
|
|
||||||
struct Sparkle {
|
struct Sparkle {
|
||||||
x: u16,
|
x: u16,
|
||||||
@@ -47,17 +41,17 @@ impl Sparkles {
|
|||||||
|
|
||||||
impl Widget for &Sparkles {
|
impl Widget for &Sparkles {
|
||||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||||
for sparkle in &self.sparkles {
|
for sp in &self.sparkles {
|
||||||
let color = COLORS[sparkle.char_idx % COLORS.len()];
|
let color = sparkle::COLORS[sp.char_idx % sparkle::COLORS.len()];
|
||||||
let intensity = (sparkle.life as f32 / 30.0).min(1.0);
|
let intensity = (sp.life as f32 / 30.0).min(1.0);
|
||||||
let r = (color.0 as f32 * intensity) as u8;
|
let r = (color.0 as f32 * intensity) as u8;
|
||||||
let g = (color.1 as f32 * intensity) as u8;
|
let g = (color.1 as f32 * intensity) as u8;
|
||||||
let b = (color.2 as f32 * intensity) as u8;
|
let b = (color.2 as f32 * intensity) as u8;
|
||||||
|
|
||||||
if sparkle.x < area.width && sparkle.y < area.height {
|
if sp.x < area.width && sp.y < area.height {
|
||||||
let x = area.x + sparkle.x;
|
let x = area.x + sp.x;
|
||||||
let y = area.y + sparkle.y;
|
let y = area.y + sp.y;
|
||||||
let ch = CHARS[sparkle.char_idx];
|
let ch = CHARS[sp.char_idx];
|
||||||
buf[(x, y)].set_char(ch).set_style(Style::new().fg(Color::Rgb(r, g, b)));
|
buf[(x, y)].set_char(ch).set_style(Style::new().fg(Color::Rgb(r, g, b)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use crate::theme::meter;
|
||||||
use ratatui::buffer::Buffer;
|
use ratatui::buffer::Buffer;
|
||||||
use ratatui::layout::Rect;
|
use ratatui::layout::Rect;
|
||||||
use ratatui::style::Color;
|
use ratatui::style::Color;
|
||||||
@@ -39,11 +40,11 @@ impl Widget for Spectrum<'_> {
|
|||||||
let y = area.y + area.height - 1 - row as u16;
|
let y = area.y + area.height - 1 - row as u16;
|
||||||
let ratio = row as f32 / area.height as f32;
|
let ratio = row as f32 / area.height as f32;
|
||||||
let color = if ratio < 0.33 {
|
let color = if ratio < 0.33 {
|
||||||
Color::Rgb(40, 180, 80)
|
Color::Rgb(meter::LOW_RGB.0, meter::LOW_RGB.1, meter::LOW_RGB.2)
|
||||||
} else if ratio < 0.66 {
|
} else if ratio < 0.66 {
|
||||||
Color::Rgb(220, 180, 40)
|
Color::Rgb(meter::MID_RGB.0, meter::MID_RGB.1, meter::MID_RGB.2)
|
||||||
} else {
|
} else {
|
||||||
Color::Rgb(220, 60, 40)
|
Color::Rgb(meter::HIGH_RGB.0, meter::HIGH_RGB.1, meter::HIGH_RGB.2)
|
||||||
};
|
};
|
||||||
for dx in 0..band_width as u16 {
|
for dx in 0..band_width as u16 {
|
||||||
let x = x_start + dx;
|
let x = x_start + dx;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use crate::theme::{input, ui};
|
||||||
use ratatui::layout::{Constraint, Layout, Rect};
|
use ratatui::layout::{Constraint, Layout, Rect};
|
||||||
use ratatui::style::{Color, Style};
|
use ratatui::style::{Color, Style};
|
||||||
use ratatui::text::{Line, Span};
|
use ratatui::text::{Line, Span};
|
||||||
@@ -20,7 +21,7 @@ impl<'a> TextInputModal<'a> {
|
|||||||
title,
|
title,
|
||||||
input,
|
input,
|
||||||
hint: None,
|
hint: None,
|
||||||
border_color: Color::White,
|
border_color: ui::TEXT_PRIMARY,
|
||||||
width: 50,
|
width: 50,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -56,15 +57,15 @@ impl<'a> TextInputModal<'a> {
|
|||||||
frame.render_widget(
|
frame.render_widget(
|
||||||
Paragraph::new(Line::from(vec![
|
Paragraph::new(Line::from(vec![
|
||||||
Span::raw("> "),
|
Span::raw("> "),
|
||||||
Span::styled(self.input, Style::new().fg(Color::Cyan)),
|
Span::styled(self.input, Style::new().fg(input::TEXT)),
|
||||||
Span::styled("█", Style::new().fg(Color::White)),
|
Span::styled("█", Style::new().fg(input::CURSOR)),
|
||||||
])),
|
])),
|
||||||
rows[0],
|
rows[0],
|
||||||
);
|
);
|
||||||
|
|
||||||
if let Some(hint) = self.hint {
|
if let Some(hint) = self.hint {
|
||||||
frame.render_widget(
|
frame.render_widget(
|
||||||
Paragraph::new(Span::styled(hint, Style::new().fg(Color::DarkGray))),
|
Paragraph::new(Span::styled(hint, Style::new().fg(input::HINT))),
|
||||||
rows[1],
|
rows[1],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -72,8 +73,8 @@ impl<'a> TextInputModal<'a> {
|
|||||||
frame.render_widget(
|
frame.render_widget(
|
||||||
Paragraph::new(Line::from(vec![
|
Paragraph::new(Line::from(vec![
|
||||||
Span::raw("> "),
|
Span::raw("> "),
|
||||||
Span::styled(self.input, Style::new().fg(Color::Cyan)),
|
Span::styled(self.input, Style::new().fg(input::TEXT)),
|
||||||
Span::styled("█", Style::new().fg(Color::White)),
|
Span::styled("█", Style::new().fg(input::CURSOR)),
|
||||||
])),
|
])),
|
||||||
inner,
|
inner,
|
||||||
);
|
);
|
||||||
|
|||||||
2631
crates/ratatui/src/theme.rs
Normal file
2631
crates/ratatui/src/theme.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,4 @@
|
|||||||
|
use crate::theme::meter;
|
||||||
use ratatui::buffer::Buffer;
|
use ratatui::buffer::Buffer;
|
||||||
use ratatui::layout::Rect;
|
use ratatui::layout::Rect;
|
||||||
use ratatui::style::Color;
|
use ratatui::style::Color;
|
||||||
@@ -31,11 +32,11 @@ impl VuMeter {
|
|||||||
|
|
||||||
fn row_to_color(row_position: f32) -> Color {
|
fn row_to_color(row_position: f32) -> Color {
|
||||||
if row_position > 0.9 {
|
if row_position > 0.9 {
|
||||||
Color::Red
|
meter::HIGH
|
||||||
} else if row_position > 0.75 {
|
} else if row_position > 0.75 {
|
||||||
Color::Yellow
|
meter::MID
|
||||||
} else {
|
} else {
|
||||||
Color::Green
|
meter::LOW
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,6 +100,8 @@ 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()
|
||||||
},
|
},
|
||||||
link: crate::settings::LinkSettings {
|
link: crate::settings::LinkSettings {
|
||||||
enabled: link.is_enabled(),
|
enabled: link.is_enabled(),
|
||||||
|
|||||||
493
src/bin/desktop.rs
Normal file
493
src/bin/desktop.rs
Normal file
@@ -0,0 +1,493 @@
|
|||||||
|
use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU32, AtomicU64, Ordering};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use clap::Parser;
|
||||||
|
use doux::EngineMetrics;
|
||||||
|
use eframe::NativeOptions;
|
||||||
|
use egui_ratatui::RataguiBackend;
|
||||||
|
use ratatui::Terminal;
|
||||||
|
use soft_ratatui::embedded_graphics_unicodefonts::{
|
||||||
|
mono_6x13_atlas, mono_6x13_bold_atlas, mono_6x13_italic_atlas,
|
||||||
|
mono_7x13_atlas, mono_7x13_bold_atlas, mono_7x13_italic_atlas,
|
||||||
|
mono_8x13_atlas, mono_8x13_bold_atlas, mono_8x13_italic_atlas,
|
||||||
|
mono_9x15_atlas, mono_9x15_bold_atlas,
|
||||||
|
mono_9x18_atlas, mono_9x18_bold_atlas,
|
||||||
|
mono_10x20_atlas,
|
||||||
|
};
|
||||||
|
use soft_ratatui::{EmbeddedGraphics, SoftBackend};
|
||||||
|
|
||||||
|
use cagire::app::App;
|
||||||
|
use cagire::engine::{
|
||||||
|
build_stream, spawn_sequencer, AnalysisHandle, AudioStreamConfig, LinkState, ScopeBuffer,
|
||||||
|
SequencerConfig, SequencerHandle, SpectrumBuffer,
|
||||||
|
};
|
||||||
|
use cagire::input::{handle_key, InputContext, InputResult};
|
||||||
|
use cagire::input_egui::convert_egui_events;
|
||||||
|
use cagire::settings::Settings;
|
||||||
|
use cagire::state::audio::RefreshRate;
|
||||||
|
use cagire::views;
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
#[command(name = "cagire-desktop", about = "Cagire desktop application")]
|
||||||
|
struct Args {
|
||||||
|
#[arg(short, long)]
|
||||||
|
samples: Vec<std::path::PathBuf>,
|
||||||
|
|
||||||
|
#[arg(short, long)]
|
||||||
|
output: Option<String>,
|
||||||
|
|
||||||
|
#[arg(short, long)]
|
||||||
|
input: Option<String>,
|
||||||
|
|
||||||
|
#[arg(short, long)]
|
||||||
|
channels: Option<u16>,
|
||||||
|
|
||||||
|
#[arg(short, long)]
|
||||||
|
buffer: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq)]
|
||||||
|
enum FontChoice {
|
||||||
|
Size6x13,
|
||||||
|
Size7x13,
|
||||||
|
Size8x13,
|
||||||
|
Size9x15,
|
||||||
|
Size9x18,
|
||||||
|
Size10x20,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FontChoice {
|
||||||
|
fn from_setting(s: &str) -> Self {
|
||||||
|
match s {
|
||||||
|
"6x13" => Self::Size6x13,
|
||||||
|
"7x13" => Self::Size7x13,
|
||||||
|
"9x15" => Self::Size9x15,
|
||||||
|
"9x18" => Self::Size9x18,
|
||||||
|
"10x20" => Self::Size10x20,
|
||||||
|
_ => Self::Size8x13,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_setting(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Size6x13 => "6x13",
|
||||||
|
Self::Size7x13 => "7x13",
|
||||||
|
Self::Size8x13 => "8x13",
|
||||||
|
Self::Size9x15 => "9x15",
|
||||||
|
Self::Size9x18 => "9x18",
|
||||||
|
Self::Size10x20 => "10x20",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn label(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Size6x13 => "6x13 (Compact)",
|
||||||
|
Self::Size7x13 => "7x13",
|
||||||
|
Self::Size8x13 => "8x13 (Default)",
|
||||||
|
Self::Size9x15 => "9x15",
|
||||||
|
Self::Size9x18 => "9x18",
|
||||||
|
Self::Size10x20 => "10x20 (Large)",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ALL: [Self; 6] = [
|
||||||
|
Self::Size6x13,
|
||||||
|
Self::Size7x13,
|
||||||
|
Self::Size8x13,
|
||||||
|
Self::Size9x15,
|
||||||
|
Self::Size9x18,
|
||||||
|
Self::Size10x20,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
type TerminalType = Terminal<RataguiBackend<EmbeddedGraphics>>;
|
||||||
|
|
||||||
|
fn create_terminal(font: FontChoice) -> TerminalType {
|
||||||
|
let (regular, bold, italic) = match font {
|
||||||
|
FontChoice::Size6x13 => (
|
||||||
|
mono_6x13_atlas(),
|
||||||
|
Some(mono_6x13_bold_atlas()),
|
||||||
|
Some(mono_6x13_italic_atlas()),
|
||||||
|
),
|
||||||
|
FontChoice::Size7x13 => (
|
||||||
|
mono_7x13_atlas(),
|
||||||
|
Some(mono_7x13_bold_atlas()),
|
||||||
|
Some(mono_7x13_italic_atlas()),
|
||||||
|
),
|
||||||
|
FontChoice::Size8x13 => (
|
||||||
|
mono_8x13_atlas(),
|
||||||
|
Some(mono_8x13_bold_atlas()),
|
||||||
|
Some(mono_8x13_italic_atlas()),
|
||||||
|
),
|
||||||
|
FontChoice::Size9x15 => (mono_9x15_atlas(), Some(mono_9x15_bold_atlas()), None),
|
||||||
|
FontChoice::Size9x18 => (mono_9x18_atlas(), Some(mono_9x18_bold_atlas()), None),
|
||||||
|
FontChoice::Size10x20 => (mono_10x20_atlas(), None, None),
|
||||||
|
};
|
||||||
|
|
||||||
|
let soft = SoftBackend::<EmbeddedGraphics>::new(80, 24, regular, bold, italic);
|
||||||
|
Terminal::new(RataguiBackend::new("cagire", soft)).expect("terminal")
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CagireDesktop {
|
||||||
|
app: App,
|
||||||
|
terminal: TerminalType,
|
||||||
|
link: Arc<LinkState>,
|
||||||
|
sequencer: Option<SequencerHandle>,
|
||||||
|
playing: Arc<AtomicBool>,
|
||||||
|
nudge_us: Arc<AtomicI64>,
|
||||||
|
lookahead_ms: Arc<AtomicU32>,
|
||||||
|
metrics: Arc<EngineMetrics>,
|
||||||
|
scope_buffer: Arc<ScopeBuffer>,
|
||||||
|
spectrum_buffer: Arc<SpectrumBuffer>,
|
||||||
|
audio_sample_pos: Arc<AtomicU64>,
|
||||||
|
sample_rate_shared: Arc<AtomicU32>,
|
||||||
|
_stream: Option<cpal::Stream>,
|
||||||
|
_analysis_handle: Option<AnalysisHandle>,
|
||||||
|
current_font: FontChoice,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CagireDesktop {
|
||||||
|
fn new(cc: &eframe::CreationContext<'_>, args: Args) -> Self {
|
||||||
|
let settings = Settings::load();
|
||||||
|
|
||||||
|
let link = Arc::new(LinkState::new(settings.link.tempo, settings.link.quantum));
|
||||||
|
if settings.link.enabled {
|
||||||
|
link.enable();
|
||||||
|
}
|
||||||
|
|
||||||
|
let playing = Arc::new(AtomicBool::new(true));
|
||||||
|
let nudge_us = Arc::new(AtomicI64::new(0));
|
||||||
|
|
||||||
|
let mut app = App::new();
|
||||||
|
|
||||||
|
app.playback
|
||||||
|
.queued_changes
|
||||||
|
.push(cagire::state::StagedChange {
|
||||||
|
change: cagire::engine::PatternChange::Start {
|
||||||
|
bank: 0,
|
||||||
|
pattern: 0,
|
||||||
|
},
|
||||||
|
quantization: cagire::model::LaunchQuantization::Immediate,
|
||||||
|
sync_mode: cagire::model::SyncMode::Reset,
|
||||||
|
});
|
||||||
|
|
||||||
|
app.audio.config.output_device = args.output.or(settings.audio.output_device);
|
||||||
|
app.audio.config.input_device = args.input.or(settings.audio.input_device);
|
||||||
|
app.audio.config.channels = args.channels.unwrap_or(settings.audio.channels);
|
||||||
|
app.audio.config.buffer_size = args.buffer.unwrap_or(settings.audio.buffer_size);
|
||||||
|
app.audio.config.max_voices = settings.audio.max_voices;
|
||||||
|
app.audio.config.lookahead_ms = settings.audio.lookahead_ms;
|
||||||
|
app.audio.config.sample_paths = args.samples;
|
||||||
|
app.audio.config.refresh_rate = RefreshRate::from_fps(settings.display.fps);
|
||||||
|
app.ui.runtime_highlight = settings.display.runtime_highlight;
|
||||||
|
app.audio.config.show_scope = settings.display.show_scope;
|
||||||
|
app.audio.config.show_spectrum = settings.display.show_spectrum;
|
||||||
|
app.ui.show_completion = settings.display.show_completion;
|
||||||
|
app.ui.flash_brightness = settings.display.flash_brightness;
|
||||||
|
|
||||||
|
let metrics = Arc::new(EngineMetrics::default());
|
||||||
|
let scope_buffer = Arc::new(ScopeBuffer::new());
|
||||||
|
let spectrum_buffer = Arc::new(SpectrumBuffer::new());
|
||||||
|
|
||||||
|
let audio_sample_pos = Arc::new(AtomicU64::new(0));
|
||||||
|
let sample_rate_shared = Arc::new(AtomicU32::new(44100));
|
||||||
|
let lookahead_ms = Arc::new(AtomicU32::new(settings.audio.lookahead_ms));
|
||||||
|
|
||||||
|
let mut initial_samples = Vec::new();
|
||||||
|
for path in &app.audio.config.sample_paths {
|
||||||
|
let index = doux::sampling::scan_samples_dir(path);
|
||||||
|
app.audio.config.sample_count += index.len();
|
||||||
|
initial_samples.extend(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
let seq_config = SequencerConfig {
|
||||||
|
audio_sample_pos: Arc::clone(&audio_sample_pos),
|
||||||
|
sample_rate: Arc::clone(&sample_rate_shared),
|
||||||
|
lookahead_ms: Arc::clone(&lookahead_ms),
|
||||||
|
};
|
||||||
|
|
||||||
|
let (sequencer, initial_audio_rx) = spawn_sequencer(
|
||||||
|
Arc::clone(&link),
|
||||||
|
Arc::clone(&playing),
|
||||||
|
Arc::clone(&app.variables),
|
||||||
|
Arc::clone(&app.dict),
|
||||||
|
Arc::clone(&app.rng),
|
||||||
|
settings.link.quantum,
|
||||||
|
Arc::clone(&app.live_keys),
|
||||||
|
Arc::clone(&nudge_us),
|
||||||
|
seq_config,
|
||||||
|
);
|
||||||
|
|
||||||
|
let stream_config = AudioStreamConfig {
|
||||||
|
output_device: app.audio.config.output_device.clone(),
|
||||||
|
channels: app.audio.config.channels,
|
||||||
|
buffer_size: app.audio.config.buffer_size,
|
||||||
|
max_voices: app.audio.config.max_voices,
|
||||||
|
};
|
||||||
|
|
||||||
|
let (stream, analysis_handle) = match build_stream(
|
||||||
|
&stream_config,
|
||||||
|
initial_audio_rx,
|
||||||
|
Arc::clone(&scope_buffer),
|
||||||
|
Arc::clone(&spectrum_buffer),
|
||||||
|
Arc::clone(&metrics),
|
||||||
|
initial_samples,
|
||||||
|
Arc::clone(&audio_sample_pos),
|
||||||
|
) {
|
||||||
|
Ok((s, sample_rate, analysis)) => {
|
||||||
|
app.audio.config.sample_rate = sample_rate;
|
||||||
|
sample_rate_shared.store(sample_rate as u32, Ordering::Relaxed);
|
||||||
|
(Some(s), Some(analysis))
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
app.ui.set_status(format!("Audio failed: {e}"));
|
||||||
|
app.audio.error = Some(e);
|
||||||
|
(None, None)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
app.mark_all_patterns_dirty();
|
||||||
|
|
||||||
|
let current_font = FontChoice::from_setting(&settings.display.font);
|
||||||
|
let terminal = create_terminal(current_font);
|
||||||
|
|
||||||
|
cc.egui_ctx.set_visuals(egui::Visuals::dark());
|
||||||
|
|
||||||
|
Self {
|
||||||
|
app,
|
||||||
|
terminal,
|
||||||
|
link,
|
||||||
|
sequencer: Some(sequencer),
|
||||||
|
playing,
|
||||||
|
nudge_us,
|
||||||
|
lookahead_ms,
|
||||||
|
metrics,
|
||||||
|
scope_buffer,
|
||||||
|
spectrum_buffer,
|
||||||
|
audio_sample_pos,
|
||||||
|
sample_rate_shared,
|
||||||
|
_stream: stream,
|
||||||
|
_analysis_handle: analysis_handle,
|
||||||
|
current_font,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_audio_restart(&mut self) {
|
||||||
|
if !self.app.audio.restart_pending {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.app.audio.restart_pending = false;
|
||||||
|
self._stream = None;
|
||||||
|
self._analysis_handle = None;
|
||||||
|
|
||||||
|
let Some(ref sequencer) = self.sequencer else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let new_audio_rx = sequencer.swap_audio_channel();
|
||||||
|
|
||||||
|
let new_config = AudioStreamConfig {
|
||||||
|
output_device: self.app.audio.config.output_device.clone(),
|
||||||
|
channels: self.app.audio.config.channels,
|
||||||
|
buffer_size: self.app.audio.config.buffer_size,
|
||||||
|
max_voices: self.app.audio.config.max_voices,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut restart_samples = Vec::new();
|
||||||
|
for path in &self.app.audio.config.sample_paths {
|
||||||
|
let index = doux::sampling::scan_samples_dir(path);
|
||||||
|
restart_samples.extend(index);
|
||||||
|
}
|
||||||
|
self.app.audio.config.sample_count = restart_samples.len();
|
||||||
|
|
||||||
|
self.audio_sample_pos.store(0, Ordering::Release);
|
||||||
|
|
||||||
|
match build_stream(
|
||||||
|
&new_config,
|
||||||
|
new_audio_rx,
|
||||||
|
Arc::clone(&self.scope_buffer),
|
||||||
|
Arc::clone(&self.spectrum_buffer),
|
||||||
|
Arc::clone(&self.metrics),
|
||||||
|
restart_samples,
|
||||||
|
Arc::clone(&self.audio_sample_pos),
|
||||||
|
) {
|
||||||
|
Ok((new_stream, sr, new_analysis)) => {
|
||||||
|
self._stream = Some(new_stream);
|
||||||
|
self._analysis_handle = Some(new_analysis);
|
||||||
|
self.app.audio.config.sample_rate = sr;
|
||||||
|
self.sample_rate_shared.store(sr as u32, Ordering::Relaxed);
|
||||||
|
self.app.audio.error = None;
|
||||||
|
self.app.ui.set_status("Audio restarted".to_string());
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
self.app.audio.error = Some(e.clone());
|
||||||
|
self.app.ui.set_status(format!("Audio failed: {e}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_metrics(&mut self) {
|
||||||
|
self.app.playback.playing = self.playing.load(Ordering::Relaxed);
|
||||||
|
|
||||||
|
self.app.metrics.active_voices =
|
||||||
|
self.metrics.active_voices.load(Ordering::Relaxed) as usize;
|
||||||
|
self.app.metrics.peak_voices = self.app.metrics.peak_voices.max(self.app.metrics.active_voices);
|
||||||
|
self.app.metrics.cpu_load = self.metrics.load.get_load();
|
||||||
|
self.app.metrics.schedule_depth =
|
||||||
|
self.metrics.schedule_depth.load(Ordering::Relaxed) as usize;
|
||||||
|
self.app.metrics.scope = self.scope_buffer.read();
|
||||||
|
(self.app.metrics.peak_left, self.app.metrics.peak_right) = self.scope_buffer.peaks();
|
||||||
|
self.app.metrics.spectrum = self.spectrum_buffer.read();
|
||||||
|
self.app.metrics.nudge_ms = self.nudge_us.load(Ordering::Relaxed) as f64 / 1000.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_input(&mut self, ctx: &egui::Context) -> bool {
|
||||||
|
let Some(ref sequencer) = self.sequencer else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let seq_snapshot = sequencer.snapshot();
|
||||||
|
|
||||||
|
for key in convert_egui_events(ctx) {
|
||||||
|
let mut input_ctx = InputContext {
|
||||||
|
app: &mut self.app,
|
||||||
|
link: &self.link,
|
||||||
|
snapshot: &seq_snapshot,
|
||||||
|
playing: &self.playing,
|
||||||
|
audio_tx: &sequencer.audio_tx,
|
||||||
|
seq_cmd_tx: &sequencer.cmd_tx,
|
||||||
|
nudge_us: &self.nudge_us,
|
||||||
|
lookahead_ms: &self.lookahead_ms,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let InputResult::Quit = handle_key(&mut input_ctx, key) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl eframe::App for CagireDesktop {
|
||||||
|
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||||
|
self.handle_audio_restart();
|
||||||
|
self.update_metrics();
|
||||||
|
|
||||||
|
let Some(ref sequencer) = self.sequencer else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let seq_snapshot = sequencer.snapshot();
|
||||||
|
|
||||||
|
self.app.metrics.event_count = seq_snapshot.event_count;
|
||||||
|
self.app.metrics.dropped_events = seq_snapshot.dropped_events;
|
||||||
|
|
||||||
|
self.app.ui.event_flash = (self.app.ui.event_flash - 0.1).max(0.0);
|
||||||
|
let new_events = self
|
||||||
|
.app
|
||||||
|
.metrics
|
||||||
|
.event_count
|
||||||
|
.saturating_sub(self.app.ui.last_event_count);
|
||||||
|
if new_events > 0 {
|
||||||
|
self.app.ui.event_flash = (new_events as f32 * 0.4).min(1.0);
|
||||||
|
}
|
||||||
|
self.app.ui.last_event_count = self.app.metrics.event_count;
|
||||||
|
|
||||||
|
self.app.flush_queued_changes(&sequencer.cmd_tx);
|
||||||
|
self.app.flush_dirty_patterns(&sequencer.cmd_tx);
|
||||||
|
|
||||||
|
let should_quit = self.handle_input(ctx);
|
||||||
|
if should_quit {
|
||||||
|
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let current_font = self.current_font;
|
||||||
|
let mut new_font = None;
|
||||||
|
|
||||||
|
egui::CentralPanel::default()
|
||||||
|
.frame(egui::Frame::NONE.fill(egui::Color32::BLACK))
|
||||||
|
.show(ctx, |ui| {
|
||||||
|
if self.app.ui.show_title {
|
||||||
|
self.app.ui.sparkles.tick(self.terminal.get_frame().area());
|
||||||
|
}
|
||||||
|
|
||||||
|
let link = &self.link;
|
||||||
|
let app = &self.app;
|
||||||
|
self.terminal
|
||||||
|
.draw(|frame| views::render(frame, app, link, &seq_snapshot))
|
||||||
|
.expect("Failed to draw");
|
||||||
|
|
||||||
|
ui.add(self.terminal.backend_mut());
|
||||||
|
|
||||||
|
let response = ui.interact(
|
||||||
|
ui.max_rect(),
|
||||||
|
egui::Id::new("terminal_context"),
|
||||||
|
egui::Sense::click(),
|
||||||
|
);
|
||||||
|
response.context_menu(|ui| {
|
||||||
|
ui.menu_button("Font", |ui| {
|
||||||
|
for choice in FontChoice::ALL {
|
||||||
|
let selected = current_font == choice;
|
||||||
|
if ui.selectable_label(selected, choice.label()).clicked() {
|
||||||
|
new_font = Some(choice);
|
||||||
|
ui.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(font) = new_font {
|
||||||
|
self.terminal = create_terminal(font);
|
||||||
|
self.current_font = font;
|
||||||
|
let mut settings = Settings::load();
|
||||||
|
settings.display.font = font.to_setting().to_string();
|
||||||
|
settings.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.request_repaint_after(Duration::from_millis(
|
||||||
|
self.app.audio.config.refresh_rate.millis(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_exit(&mut self, _gl: Option<&eframe::glow::Context>) {
|
||||||
|
if let Some(sequencer) = self.sequencer.take() {
|
||||||
|
sequencer.shutdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_icon() -> egui::IconData {
|
||||||
|
const ICON_BYTES: &[u8] = include_bytes!("../../cagire_pixel.png");
|
||||||
|
|
||||||
|
let img = image::load_from_memory(ICON_BYTES)
|
||||||
|
.expect("Failed to load embedded icon")
|
||||||
|
.resize(64, 64, image::imageops::FilterType::Lanczos3)
|
||||||
|
.into_rgba8();
|
||||||
|
|
||||||
|
let (width, height) = img.dimensions();
|
||||||
|
|
||||||
|
egui::IconData {
|
||||||
|
rgba: img.into_raw(),
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> eframe::Result<()> {
|
||||||
|
let args = Args::parse();
|
||||||
|
|
||||||
|
let options = NativeOptions {
|
||||||
|
viewport: egui::ViewportBuilder::default()
|
||||||
|
.with_title("Cagire")
|
||||||
|
.with_inner_size([1200.0, 800.0])
|
||||||
|
.with_icon(std::sync::Arc::new(load_icon())),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
eframe::run_native(
|
||||||
|
"Cagire",
|
||||||
|
options,
|
||||||
|
Box::new(move |cc| Ok(Box::new(CagireDesktop::new(cc, args)))),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
mod audio;
|
mod audio;
|
||||||
mod link;
|
mod link;
|
||||||
mod sequencer;
|
pub mod sequencer;
|
||||||
|
|
||||||
pub use audio::{build_stream, AudioStreamConfig, ScopeBuffer, SpectrumBuffer};
|
pub use audio::{build_stream, AnalysisHandle, AudioStreamConfig, ScopeBuffer, SpectrumBuffer};
|
||||||
pub use link::LinkState;
|
pub use link::LinkState;
|
||||||
pub use sequencer::{
|
pub use sequencer::{
|
||||||
spawn_sequencer, AudioCommand, PatternChange, PatternSnapshot, SeqCommand, SequencerConfig,
|
spawn_sequencer, AudioCommand, PatternChange, PatternSnapshot, SeqCommand, SequencerConfig,
|
||||||
SequencerSnapshot, StepSnapshot,
|
SequencerHandle, SequencerSnapshot, StepSnapshot,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -165,7 +165,9 @@ impl SequencerHandle {
|
|||||||
|
|
||||||
pub fn shutdown(self) {
|
pub fn shutdown(self) {
|
||||||
let _ = self.cmd_tx.send(SeqCommand::Shutdown);
|
let _ = self.cmd_tx.send(SeqCommand::Shutdown);
|
||||||
let _ = self.thread.join();
|
if let Err(e) = self.thread.join() {
|
||||||
|
eprintln!("Sequencer thread panicked: {e:?}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -873,7 +875,7 @@ fn sequencer_loop(
|
|||||||
let tempo = state.tempo();
|
let tempo = state.tempo();
|
||||||
|
|
||||||
let sr = sample_rate.load(Ordering::Relaxed) as f64;
|
let sr = sample_rate.load(Ordering::Relaxed) as f64;
|
||||||
let audio_samples = audio_sample_pos.load(Ordering::Relaxed);
|
let audio_samples = audio_sample_pos.load(Ordering::Acquire);
|
||||||
let engine_time = if sr > 0.0 {
|
let engine_time = if sr > 0.0 {
|
||||||
audio_samples as f64 / sr
|
audio_samples as f64 / sr
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
193
src/input_egui.rs
Normal file
193
src/input_egui.rs
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||||
|
|
||||||
|
pub fn convert_egui_events(ctx: &egui::Context) -> Vec<KeyEvent> {
|
||||||
|
let mut events = Vec::new();
|
||||||
|
|
||||||
|
for event in &ctx.input(|i| i.events.clone()) {
|
||||||
|
if let Some(key_event) = convert_event(event) {
|
||||||
|
events.push(key_event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
events
|
||||||
|
}
|
||||||
|
|
||||||
|
fn convert_event(event: &egui::Event) -> Option<KeyEvent> {
|
||||||
|
match event {
|
||||||
|
egui::Event::Key {
|
||||||
|
key,
|
||||||
|
pressed,
|
||||||
|
modifiers,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
if !*pressed {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mods = convert_modifiers(*modifiers);
|
||||||
|
// For character keys without ctrl/alt, let Event::Text handle it
|
||||||
|
if is_character_key(*key) && !mods.intersects(KeyModifiers::CONTROL | KeyModifiers::ALT) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let code = convert_key(*key)?;
|
||||||
|
Some(KeyEvent::new(code, mods))
|
||||||
|
}
|
||||||
|
egui::Event::Text(text) => {
|
||||||
|
if text.len() == 1 {
|
||||||
|
let c = text.chars().next()?;
|
||||||
|
if !c.is_control() {
|
||||||
|
return Some(KeyEvent::new(KeyCode::Char(c), KeyModifiers::empty()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn convert_key(key: egui::Key) -> Option<KeyCode> {
|
||||||
|
Some(match key {
|
||||||
|
egui::Key::ArrowDown => KeyCode::Down,
|
||||||
|
egui::Key::ArrowLeft => KeyCode::Left,
|
||||||
|
egui::Key::ArrowRight => KeyCode::Right,
|
||||||
|
egui::Key::ArrowUp => KeyCode::Up,
|
||||||
|
egui::Key::Escape => KeyCode::Esc,
|
||||||
|
egui::Key::Tab => KeyCode::Tab,
|
||||||
|
egui::Key::Backspace => KeyCode::Backspace,
|
||||||
|
egui::Key::Enter => KeyCode::Enter,
|
||||||
|
egui::Key::Space => KeyCode::Char(' '),
|
||||||
|
egui::Key::Insert => KeyCode::Insert,
|
||||||
|
egui::Key::Delete => KeyCode::Delete,
|
||||||
|
egui::Key::Home => KeyCode::Home,
|
||||||
|
egui::Key::End => KeyCode::End,
|
||||||
|
egui::Key::PageUp => KeyCode::PageUp,
|
||||||
|
egui::Key::PageDown => KeyCode::PageDown,
|
||||||
|
egui::Key::F1 => KeyCode::F(1),
|
||||||
|
egui::Key::F2 => KeyCode::F(2),
|
||||||
|
egui::Key::F3 => KeyCode::F(3),
|
||||||
|
egui::Key::F4 => KeyCode::F(4),
|
||||||
|
egui::Key::F5 => KeyCode::F(5),
|
||||||
|
egui::Key::F6 => KeyCode::F(6),
|
||||||
|
egui::Key::F7 => KeyCode::F(7),
|
||||||
|
egui::Key::F8 => KeyCode::F(8),
|
||||||
|
egui::Key::F9 => KeyCode::F(9),
|
||||||
|
egui::Key::F10 => KeyCode::F(10),
|
||||||
|
egui::Key::F11 => KeyCode::F(11),
|
||||||
|
egui::Key::F12 => KeyCode::F(12),
|
||||||
|
egui::Key::A => KeyCode::Char('a'),
|
||||||
|
egui::Key::B => KeyCode::Char('b'),
|
||||||
|
egui::Key::C => KeyCode::Char('c'),
|
||||||
|
egui::Key::D => KeyCode::Char('d'),
|
||||||
|
egui::Key::E => KeyCode::Char('e'),
|
||||||
|
egui::Key::F => KeyCode::Char('f'),
|
||||||
|
egui::Key::G => KeyCode::Char('g'),
|
||||||
|
egui::Key::H => KeyCode::Char('h'),
|
||||||
|
egui::Key::I => KeyCode::Char('i'),
|
||||||
|
egui::Key::J => KeyCode::Char('j'),
|
||||||
|
egui::Key::K => KeyCode::Char('k'),
|
||||||
|
egui::Key::L => KeyCode::Char('l'),
|
||||||
|
egui::Key::M => KeyCode::Char('m'),
|
||||||
|
egui::Key::N => KeyCode::Char('n'),
|
||||||
|
egui::Key::O => KeyCode::Char('o'),
|
||||||
|
egui::Key::P => KeyCode::Char('p'),
|
||||||
|
egui::Key::Q => KeyCode::Char('q'),
|
||||||
|
egui::Key::R => KeyCode::Char('r'),
|
||||||
|
egui::Key::S => KeyCode::Char('s'),
|
||||||
|
egui::Key::T => KeyCode::Char('t'),
|
||||||
|
egui::Key::U => KeyCode::Char('u'),
|
||||||
|
egui::Key::V => KeyCode::Char('v'),
|
||||||
|
egui::Key::W => KeyCode::Char('w'),
|
||||||
|
egui::Key::X => KeyCode::Char('x'),
|
||||||
|
egui::Key::Y => KeyCode::Char('y'),
|
||||||
|
egui::Key::Z => KeyCode::Char('z'),
|
||||||
|
egui::Key::Num0 => KeyCode::Char('0'),
|
||||||
|
egui::Key::Num1 => KeyCode::Char('1'),
|
||||||
|
egui::Key::Num2 => KeyCode::Char('2'),
|
||||||
|
egui::Key::Num3 => KeyCode::Char('3'),
|
||||||
|
egui::Key::Num4 => KeyCode::Char('4'),
|
||||||
|
egui::Key::Num5 => KeyCode::Char('5'),
|
||||||
|
egui::Key::Num6 => KeyCode::Char('6'),
|
||||||
|
egui::Key::Num7 => KeyCode::Char('7'),
|
||||||
|
egui::Key::Num8 => KeyCode::Char('8'),
|
||||||
|
egui::Key::Num9 => KeyCode::Char('9'),
|
||||||
|
egui::Key::Minus => KeyCode::Char('-'),
|
||||||
|
egui::Key::Equals => KeyCode::Char('='),
|
||||||
|
egui::Key::OpenBracket => KeyCode::Char('['),
|
||||||
|
egui::Key::CloseBracket => KeyCode::Char(']'),
|
||||||
|
egui::Key::Semicolon => KeyCode::Char(';'),
|
||||||
|
egui::Key::Comma => KeyCode::Char(','),
|
||||||
|
egui::Key::Period => KeyCode::Char('.'),
|
||||||
|
egui::Key::Slash => KeyCode::Char('/'),
|
||||||
|
egui::Key::Backslash => KeyCode::Char('\\'),
|
||||||
|
egui::Key::Backtick => KeyCode::Char('`'),
|
||||||
|
egui::Key::Quote => KeyCode::Char('\''),
|
||||||
|
_ => return None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn convert_modifiers(mods: egui::Modifiers) -> KeyModifiers {
|
||||||
|
let mut result = KeyModifiers::empty();
|
||||||
|
if mods.shift {
|
||||||
|
result |= KeyModifiers::SHIFT;
|
||||||
|
}
|
||||||
|
if mods.ctrl || mods.command {
|
||||||
|
result |= KeyModifiers::CONTROL;
|
||||||
|
}
|
||||||
|
if mods.alt {
|
||||||
|
result |= KeyModifiers::ALT;
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_character_key(key: egui::Key) -> bool {
|
||||||
|
matches!(
|
||||||
|
key,
|
||||||
|
egui::Key::A
|
||||||
|
| egui::Key::B
|
||||||
|
| egui::Key::C
|
||||||
|
| egui::Key::D
|
||||||
|
| egui::Key::E
|
||||||
|
| egui::Key::F
|
||||||
|
| egui::Key::G
|
||||||
|
| egui::Key::H
|
||||||
|
| egui::Key::I
|
||||||
|
| egui::Key::J
|
||||||
|
| egui::Key::K
|
||||||
|
| egui::Key::L
|
||||||
|
| egui::Key::M
|
||||||
|
| egui::Key::N
|
||||||
|
| egui::Key::O
|
||||||
|
| egui::Key::P
|
||||||
|
| egui::Key::Q
|
||||||
|
| egui::Key::R
|
||||||
|
| egui::Key::S
|
||||||
|
| egui::Key::T
|
||||||
|
| egui::Key::U
|
||||||
|
| egui::Key::V
|
||||||
|
| egui::Key::W
|
||||||
|
| egui::Key::X
|
||||||
|
| egui::Key::Y
|
||||||
|
| egui::Key::Z
|
||||||
|
| egui::Key::Num0
|
||||||
|
| egui::Key::Num1
|
||||||
|
| egui::Key::Num2
|
||||||
|
| egui::Key::Num3
|
||||||
|
| egui::Key::Num4
|
||||||
|
| egui::Key::Num5
|
||||||
|
| egui::Key::Num6
|
||||||
|
| egui::Key::Num7
|
||||||
|
| egui::Key::Num8
|
||||||
|
| egui::Key::Num9
|
||||||
|
| egui::Key::Space
|
||||||
|
| egui::Key::Minus
|
||||||
|
| egui::Key::Equals
|
||||||
|
| egui::Key::OpenBracket
|
||||||
|
| egui::Key::CloseBracket
|
||||||
|
| egui::Key::Semicolon
|
||||||
|
| egui::Key::Comma
|
||||||
|
| egui::Key::Period
|
||||||
|
| egui::Key::Slash
|
||||||
|
| egui::Key::Backslash
|
||||||
|
| egui::Key::Backtick
|
||||||
|
| egui::Key::Quote
|
||||||
|
)
|
||||||
|
}
|
||||||
15
src/lib.rs
15
src/lib.rs
@@ -1,2 +1,17 @@
|
|||||||
pub use cagire_forth as forth;
|
pub use cagire_forth as forth;
|
||||||
|
|
||||||
|
pub mod app;
|
||||||
|
pub mod commands;
|
||||||
|
pub mod engine;
|
||||||
|
pub mod input;
|
||||||
pub mod model;
|
pub mod model;
|
||||||
|
pub mod page;
|
||||||
|
pub mod services;
|
||||||
|
pub mod settings;
|
||||||
|
pub mod state;
|
||||||
|
pub mod theme;
|
||||||
|
pub mod views;
|
||||||
|
pub mod widgets;
|
||||||
|
|
||||||
|
#[cfg(feature = "desktop")]
|
||||||
|
pub mod input_egui;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ mod page;
|
|||||||
mod services;
|
mod services;
|
||||||
mod settings;
|
mod settings;
|
||||||
mod state;
|
mod state;
|
||||||
|
mod theme;
|
||||||
mod views;
|
mod views;
|
||||||
mod widgets;
|
mod widgets;
|
||||||
|
|
||||||
@@ -97,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());
|
||||||
|
|||||||
@@ -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)]
|
||||||
@@ -34,6 +36,14 @@ pub struct DisplaySettings {
|
|||||||
pub show_completion: bool,
|
pub show_completion: bool,
|
||||||
#[serde(default = "default_flash_brightness")]
|
#[serde(default = "default_flash_brightness")]
|
||||||
pub flash_brightness: f32,
|
pub flash_brightness: f32,
|
||||||
|
#[serde(default = "default_font")]
|
||||||
|
pub font: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub color_scheme: ColorScheme,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_font() -> String {
|
||||||
|
"8x13".to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_flash_brightness() -> f32 { 1.0 }
|
fn default_flash_brightness() -> f32 { 1.0 }
|
||||||
@@ -69,6 +79,8 @@ impl Default for DisplaySettings {
|
|||||||
show_spectrum: true,
|
show_spectrum: true,
|
||||||
show_completion: true,
|
show_completion: true,
|
||||||
flash_brightness: 1.0,
|
flash_brightness: 1.0,
|
||||||
|
font: default_font(),
|
||||||
|
color_scheme: ColorScheme::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -89,7 +101,9 @@ impl Settings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn save(&self) {
|
pub fn save(&self) {
|
||||||
let _ = confy::store(APP_NAME, None, self);
|
if let Err(e) = confy::store(APP_NAME, None, self) {
|
||||||
|
eprintln!("Failed to save settings: {e}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
65
src/state/color_scheme.rs
Normal file
65
src/state/color_scheme.rs
Normal 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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
3
src/theme.rs
Normal file
3
src/theme.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
//! Re-export theme from cagire-ratatui crate.
|
||||||
|
|
||||||
|
pub use cagire_ratatui::theme::*;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
use ratatui::layout::{Constraint, Layout, Rect};
|
use ratatui::layout::{Constraint, Layout, Rect};
|
||||||
use ratatui::style::{Color, Modifier, Style};
|
use ratatui::style::{Modifier, Style};
|
||||||
use ratatui::text::{Line as RLine, Span};
|
use ratatui::text::{Line as RLine, Span};
|
||||||
use ratatui::widgets::{Block, Borders, List, ListItem, Paragraph};
|
use ratatui::widgets::{Block, Borders, List, ListItem, Paragraph};
|
||||||
use ratatui::Frame;
|
use ratatui::Frame;
|
||||||
@@ -7,6 +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;
|
||||||
|
|
||||||
const CATEGORIES: &[&str] = &[
|
const CATEGORIES: &[&str] = &[
|
||||||
// Forth core
|
// Forth core
|
||||||
@@ -56,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(Color::Rgb(60, 60, 70)))
|
.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(Color::Rgb(140, 145, 155)))
|
.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
|
||||||
@@ -79,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(Color::Rgb(80, 80, 90))
|
Style::new().fg(theme.dict.category_dimmed)
|
||||||
} else if is_selected && focused {
|
} else if is_selected && focused {
|
||||||
Style::new().fg(Color::Yellow).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(Color::Cyan)
|
Style::new().fg(theme.dict.category_selected)
|
||||||
} else {
|
} else {
|
||||||
Style::new().fg(Color::White)
|
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 { Color::Yellow } else { Color::Rgb(60, 60, 70) };
|
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))
|
||||||
@@ -102,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
|
||||||
@@ -142,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 = Color::Rgb(40, 50, 60);
|
let name_bg = theme.dict.word_bg;
|
||||||
let name_style = Style::new()
|
let name_style = Style::new()
|
||||||
.fg(Color::Green)
|
.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(Color::DarkGray).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 {
|
||||||
@@ -167,19 +171,19 @@ fn render_words(frame: &mut Frame, app: &App, area: Rect, is_searching: bool) {
|
|||||||
]));
|
]));
|
||||||
}
|
}
|
||||||
|
|
||||||
let stack_style = Style::new().fg(Color::Magenta);
|
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(Color::White);
|
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(Color::Rgb(120, 130, 140));
|
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),
|
||||||
@@ -205,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 { Color::Yellow } else { Color::Rgb(60, 60, 70) };
|
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))
|
||||||
@@ -215,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(Color::Yellow)
|
Style::new().fg(theme.search.active)
|
||||||
} else {
|
} else {
|
||||||
Style::new().fg(Color::DarkGray)
|
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);
|
||||||
|
|||||||
@@ -1,18 +1,15 @@
|
|||||||
use cagire_ratatui::ListSelect;
|
use cagire_ratatui::ListSelect;
|
||||||
use ratatui::layout::{Constraint, Layout, Rect};
|
use ratatui::layout::{Constraint, Layout, Rect};
|
||||||
use ratatui::style::{Color, Modifier, Style};
|
use ratatui::style::{Modifier, Style};
|
||||||
use ratatui::text::{Line, Span};
|
use ratatui::text::{Line, Span};
|
||||||
use ratatui::widgets::{Block, Borders, Paragraph, Row, Table};
|
use ratatui::widgets::{Block, Borders, Paragraph, Row, Table};
|
||||||
use ratatui::Frame;
|
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;
|
||||||
use crate::widgets::{Orientation, Scope, Spectrum};
|
use crate::widgets::{Orientation, Scope, Spectrum};
|
||||||
|
|
||||||
const HEADER_COLOR: Color = Color::Rgb(100, 160, 180);
|
|
||||||
const DIVIDER_COLOR: Color = Color::Rgb(60, 65, 70);
|
|
||||||
const SCROLL_INDICATOR_COLOR: Color = Color::Rgb(80, 85, 95);
|
|
||||||
|
|
||||||
pub fn render(frame: &mut Frame, app: &App, area: Rect) {
|
pub fn render(frame: &mut Frame, app: &App, area: Rect) {
|
||||||
let [left_col, _, right_col] = Layout::horizontal([
|
let [left_col, _, right_col] = Layout::horizontal([
|
||||||
Constraint::Percentage(55),
|
Constraint::Percentage(55),
|
||||||
@@ -26,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(Color::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);
|
||||||
@@ -125,7 +123,7 @@ fn render_settings_section(frame: &mut Frame, app: &App, area: Rect) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Scroll indicators
|
// Scroll indicators
|
||||||
let indicator_style = Style::new().fg(SCROLL_INDICATOR_COLOR);
|
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 {
|
||||||
@@ -155,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(Color::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(Color::Green);
|
.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(Color::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);
|
||||||
@@ -206,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(Color::Yellow).add_modifier(Modifier::BOLD)
|
Style::new().fg(theme.engine.header_focused).add_modifier(Modifier::BOLD)
|
||||||
} else {
|
} else {
|
||||||
Style::new().fg(HEADER_COLOR).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(DIVIDER_COLOR)),
|
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] =
|
||||||
@@ -254,7 +256,7 @@ fn render_devices(frame: &mut Frame, app: &App, area: Rect) {
|
|||||||
section_focused,
|
section_focused,
|
||||||
);
|
);
|
||||||
|
|
||||||
let sep_style = Style::new().fg(Color::Rgb(60, 65, 75));
|
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();
|
||||||
@@ -285,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(Color::Yellow).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(Color::Rgb(150, 155, 165))
|
Style::new().fg(theme.engine.label_focused)
|
||||||
} else {
|
} else {
|
||||||
Style::new().fg(Color::Rgb(100, 105, 115))
|
Style::new().fg(theme.engine.label_dim)
|
||||||
};
|
};
|
||||||
|
|
||||||
let arrow = if focused { "> " } else { " " };
|
let arrow = if focused { "> " } else { " " };
|
||||||
@@ -311,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] =
|
||||||
@@ -318,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(Color::Yellow).add_modifier(Modifier::BOLD);
|
let highlight = Style::new().fg(theme.engine.focused).add_modifier(Modifier::BOLD);
|
||||||
let normal = Style::new().fg(Color::White);
|
let normal = Style::new().fg(theme.engine.normal);
|
||||||
let label_style = Style::new().fg(Color::Rgb(120, 125, 135));
|
let label_style = Style::new().fg(theme.engine.label);
|
||||||
let value_style = Style::new().fg(Color::Rgb(180, 180, 190));
|
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;
|
||||||
@@ -423,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([
|
||||||
@@ -438,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(Color::Rgb(80, 85, 95));
|
let dim = Style::new().fg(theme.engine.dim);
|
||||||
let path_style = Style::new().fg(Color::Rgb(120, 125, 135));
|
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() {
|
||||||
@@ -470,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(Color::Rgb(180, 180, 100))
|
Style::new().fg(theme.engine.hint_active)
|
||||||
} else {
|
} else {
|
||||||
Style::new().fg(Color::Rgb(60, 60, 70))
|
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(Color::Rgb(80, 85, 95))),
|
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(Color::Rgb(80, 85, 95))),
|
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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
use minimad::{Composite, CompositeStyle, Compound, Line};
|
use minimad::{Composite, CompositeStyle, Compound, Line};
|
||||||
use ratatui::layout::{Constraint, Layout, Rect};
|
use ratatui::layout::{Constraint, Layout, Rect};
|
||||||
use ratatui::style::{Color, Modifier, Style, Stylize};
|
use ratatui::style::{Modifier, Style};
|
||||||
use ratatui::text::{Line as RLine, Span};
|
use ratatui::text::{Line as RLine, Span};
|
||||||
use ratatui::widgets::{Block, Borders, List, ListItem, Padding, Paragraph, Wrap};
|
use ratatui::widgets::{Block, Borders, List, ListItem, Padding, Paragraph, Wrap};
|
||||||
use ratatui::Frame;
|
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;
|
||||||
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.
|
||||||
@@ -31,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(Color::Cyan).add_modifier(Modifier::BOLD)
|
Style::new().fg(theme.dict.category_selected).add_modifier(Modifier::BOLD)
|
||||||
} else {
|
} else {
|
||||||
Style::new().fg(Color::White)
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,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;
|
||||||
@@ -63,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().cyan().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(Color::White),
|
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] =
|
||||||
@@ -118,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 });
|
||||||
@@ -125,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(Color::Yellow)
|
Style::new().fg(theme.search.active)
|
||||||
} else {
|
} else {
|
||||||
Style::new().fg(Color::DarkGray)
|
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);
|
||||||
@@ -136,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();
|
||||||
@@ -145,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(Color::Yellow).fg(Color::Black);
|
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();
|
||||||
@@ -185,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(Color::Rgb(60, 60, 70))
|
let theme = theme::get();
|
||||||
|
Style::new().fg(theme.markdown.code_border)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn preprocess_underscores(md: &str) -> String {
|
fn preprocess_underscores(md: &str) -> String {
|
||||||
@@ -268,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(Color::Cyan)
|
.fg(theme.markdown.h1)
|
||||||
.add_modifier(Modifier::BOLD | Modifier::UNDERLINED),
|
.add_modifier(Modifier::BOLD | Modifier::UNDERLINED),
|
||||||
CompositeStyle::Header(2) => Style::new().fg(Color::Yellow).add_modifier(Modifier::BOLD),
|
CompositeStyle::Header(2) => Style::new().fg(theme.markdown.h2).add_modifier(Modifier::BOLD),
|
||||||
CompositeStyle::Header(_) => Style::new().fg(Color::Magenta).add_modifier(Modifier::BOLD),
|
CompositeStyle::Header(_) => Style::new().fg(theme.markdown.h3).add_modifier(Modifier::BOLD),
|
||||||
CompositeStyle::ListItem(_) => Style::new().fg(Color::White),
|
CompositeStyle::ListItem(_) => Style::new().fg(theme.markdown.list),
|
||||||
CompositeStyle::Quote => Style::new().fg(Color::Rgb(150, 150, 150)),
|
CompositeStyle::Quote => Style::new().fg(theme.markdown.quote),
|
||||||
CompositeStyle::Code => Style::new().fg(Color::Green),
|
CompositeStyle::Code => Style::new().fg(theme.markdown.code),
|
||||||
CompositeStyle::Paragraph => Style::new().fg(Color::White),
|
CompositeStyle::Paragraph => Style::new().fg(theme.markdown.text),
|
||||||
};
|
};
|
||||||
|
|
||||||
let prefix = match composite.style {
|
let prefix = match composite.style {
|
||||||
@@ -299,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 {
|
||||||
@@ -308,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(Color::Green);
|
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);
|
||||||
@@ -316,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(Color::Rgb(120, 200, 180))
|
.fg(theme.markdown.link)
|
||||||
.add_modifier(Modifier::UNDERLINED);
|
.add_modifier(Modifier::UNDERLINED);
|
||||||
|
|
||||||
let mut rest = src.as_str();
|
let mut rest = src.as_str();
|
||||||
@@ -336,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(Color::Rgb(100, 100, 100)),
|
Style::new().fg(theme.markdown.link_url),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
rest = &rest[url_start + url_end + 1..];
|
rest = &rest[url_start + url_end + 1..];
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use ratatui::style::{Color, Modifier, Style};
|
use ratatui::style::{Modifier, Style};
|
||||||
|
|
||||||
use crate::model::{SourceSpan, WordCompile, WORDS};
|
use crate::model::{SourceSpan, WordCompile, WORDS};
|
||||||
|
use crate::theme;
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum TokenKind {
|
pub enum TokenKind {
|
||||||
@@ -24,61 +25,36 @@ pub enum TokenKind {
|
|||||||
|
|
||||||
impl TokenKind {
|
impl TokenKind {
|
||||||
pub fn style(self) -> Style {
|
pub fn style(self) -> Style {
|
||||||
match self {
|
let theme = theme::get();
|
||||||
TokenKind::Emit => Style::default()
|
let (fg, bg) = match self {
|
||||||
.fg(Color::Rgb(255, 255, 255))
|
TokenKind::Emit => theme.syntax.emit,
|
||||||
.bg(Color::Rgb(140, 50, 50))
|
TokenKind::Number => theme.syntax.number,
|
||||||
.add_modifier(Modifier::BOLD),
|
TokenKind::String => theme.syntax.string,
|
||||||
TokenKind::Number => Style::default()
|
TokenKind::Comment => theme.syntax.comment,
|
||||||
.fg(Color::Rgb(255, 200, 120))
|
TokenKind::Keyword => theme.syntax.keyword,
|
||||||
.bg(Color::Rgb(60, 40, 15)),
|
TokenKind::StackOp => theme.syntax.stack_op,
|
||||||
TokenKind::String => Style::default()
|
TokenKind::Operator => theme.syntax.operator,
|
||||||
.fg(Color::Rgb(150, 230, 150))
|
TokenKind::Sound => theme.syntax.sound,
|
||||||
.bg(Color::Rgb(20, 55, 20)),
|
TokenKind::Param => theme.syntax.param,
|
||||||
TokenKind::Comment => Style::default()
|
TokenKind::Context => theme.syntax.context,
|
||||||
.fg(Color::Rgb(100, 100, 100))
|
TokenKind::Note => theme.syntax.note,
|
||||||
.bg(Color::Rgb(18, 18, 18)),
|
TokenKind::Interval => theme.syntax.interval,
|
||||||
TokenKind::Keyword => Style::default()
|
TokenKind::Variable => theme.syntax.variable,
|
||||||
.fg(Color::Rgb(230, 130, 230))
|
TokenKind::Vary => theme.syntax.vary,
|
||||||
.bg(Color::Rgb(55, 25, 55)),
|
TokenKind::Generator => theme.syntax.generator,
|
||||||
TokenKind::StackOp => Style::default()
|
TokenKind::Default => theme.syntax.default,
|
||||||
.fg(Color::Rgb(130, 190, 240))
|
};
|
||||||
.bg(Color::Rgb(20, 40, 70)),
|
let style = Style::default().fg(fg).bg(bg);
|
||||||
TokenKind::Operator => Style::default()
|
if matches!(self, TokenKind::Emit) {
|
||||||
.fg(Color::Rgb(220, 220, 140))
|
style.add_modifier(Modifier::BOLD)
|
||||||
.bg(Color::Rgb(45, 45, 20)),
|
} else {
|
||||||
TokenKind::Sound => Style::default()
|
style
|
||||||
.fg(Color::Rgb(100, 240, 220))
|
|
||||||
.bg(Color::Rgb(15, 60, 55)),
|
|
||||||
TokenKind::Param => Style::default()
|
|
||||||
.fg(Color::Rgb(190, 160, 240))
|
|
||||||
.bg(Color::Rgb(45, 30, 70)),
|
|
||||||
TokenKind::Context => Style::default()
|
|
||||||
.fg(Color::Rgb(240, 190, 120))
|
|
||||||
.bg(Color::Rgb(60, 45, 20)),
|
|
||||||
TokenKind::Note => Style::default()
|
|
||||||
.fg(Color::Rgb(120, 220, 170))
|
|
||||||
.bg(Color::Rgb(20, 55, 40)),
|
|
||||||
TokenKind::Interval => Style::default()
|
|
||||||
.fg(Color::Rgb(170, 220, 120))
|
|
||||||
.bg(Color::Rgb(35, 55, 20)),
|
|
||||||
TokenKind::Variable => Style::default()
|
|
||||||
.fg(Color::Rgb(220, 150, 190))
|
|
||||||
.bg(Color::Rgb(60, 30, 50)),
|
|
||||||
TokenKind::Vary => Style::default()
|
|
||||||
.fg(Color::Rgb(230, 230, 100))
|
|
||||||
.bg(Color::Rgb(55, 55, 15)),
|
|
||||||
TokenKind::Generator => Style::default()
|
|
||||||
.fg(Color::Rgb(100, 220, 180))
|
|
||||||
.bg(Color::Rgb(15, 55, 45)),
|
|
||||||
TokenKind::Default => Style::default()
|
|
||||||
.fg(Color::Rgb(160, 160, 160))
|
|
||||||
.bg(Color::Rgb(25, 25, 25)),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn gap_style() -> Style {
|
pub fn gap_style() -> Style {
|
||||||
Style::default().bg(Color::Rgb(25, 25, 25))
|
let theme = theme::get();
|
||||||
|
Style::default().bg(theme.syntax.gap_bg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,9 +207,6 @@ pub fn highlight_line_with_runtime(
|
|||||||
let tokens = tokenize_line(line);
|
let tokens = tokenize_line(line);
|
||||||
let mut result = Vec::new();
|
let mut result = Vec::new();
|
||||||
let mut last_end = 0;
|
let mut last_end = 0;
|
||||||
|
|
||||||
let executed_bg = Color::Rgb(40, 35, 50);
|
|
||||||
let selected_bg = Color::Rgb(80, 60, 20);
|
|
||||||
let gap_style = TokenKind::gap_style();
|
let gap_style = TokenKind::gap_style();
|
||||||
|
|
||||||
for token in tokens {
|
for token in tokens {
|
||||||
@@ -252,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(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(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()));
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ use ratatui::Frame;
|
|||||||
|
|
||||||
use crate::app::App;
|
use crate::app::App;
|
||||||
use crate::engine::SequencerSnapshot;
|
use crate::engine::SequencerSnapshot;
|
||||||
|
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) {
|
||||||
@@ -64,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(Color::Rgb(120, 125, 135)));
|
.style(Style::new().fg(theme.ui.text_muted));
|
||||||
frame.render_widget(msg, area);
|
frame.render_widget(msg, area);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -131,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);
|
||||||
@@ -147,41 +151,27 @@ 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| {
|
||||||
const BRIGHT: [(u8, u8, u8); 5] = [
|
|
||||||
(180, 140, 220),
|
|
||||||
(220, 140, 170),
|
|
||||||
(220, 180, 130),
|
|
||||||
(130, 180, 220),
|
|
||||||
(170, 220, 140),
|
|
||||||
];
|
|
||||||
const DIM: [(u8, u8, u8); 5] = [
|
|
||||||
(90, 70, 120),
|
|
||||||
(120, 70, 85),
|
|
||||||
(120, 90, 65),
|
|
||||||
(65, 90, 120),
|
|
||||||
(85, 120, 70),
|
|
||||||
];
|
|
||||||
let i = src % 5;
|
let i = src % 5;
|
||||||
(BRIGHT[i], 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, _, _, _) => (Color::Rgb(195, 85, 65), Color::White),
|
(true, true, _, _, _) => (theme.tile.playing_active_bg, theme.tile.playing_active_fg),
|
||||||
(true, false, _, _, _) => (Color::Rgb(180, 120, 45), Color::Black),
|
(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), Color::Black)
|
(Color::Rgb(r, g, b), theme.selection.cursor_fg)
|
||||||
}
|
}
|
||||||
(false, true, true, false, _) => (Color::Rgb(0, 220, 180), Color::Black),
|
(false, true, true, false, _) => (theme.tile.active_selected_bg, theme.selection.cursor_fg),
|
||||||
(false, true, _, _, true) => (Color::Rgb(0, 170, 140), Color::Black),
|
(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), Color::White)
|
(Color::Rgb(r, g, b), theme.tile.active_fg)
|
||||||
}
|
}
|
||||||
(false, true, false, false, _) => (Color::Rgb(45, 106, 95), Color::White),
|
(false, true, false, false, _) => (theme.tile.active_bg, theme.tile.active_fg),
|
||||||
(false, false, true, _, _) => (Color::Rgb(80, 180, 255), Color::Black),
|
(false, false, true, _, _) => (theme.selection.selected, theme.selection.cursor_fg),
|
||||||
(false, false, _, _, true) => (Color::Rgb(60, 140, 200), Color::Black),
|
(false, false, _, _, true) => (theme.selection.in_range, theme.selection.cursor_fg),
|
||||||
(false, false, false, _, _) => (Color::Rgb(45, 48, 55), Color::Rgb(120, 125, 135)),
|
(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);
|
||||||
@@ -244,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(Color::Green);
|
.color(theme.meter.low);
|
||||||
frame.render_widget(scope, area);
|
frame.render_widget(scope, area);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -260,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use ratatui::layout::Rect;
|
use ratatui::layout::Rect;
|
||||||
use ratatui::style::{Color, Modifier, Style};
|
use ratatui::style::{Modifier, Style};
|
||||||
use ratatui::text::{Line, Span};
|
use ratatui::text::{Line, Span};
|
||||||
use ratatui::widgets::{Block, Borders, Paragraph};
|
use ratatui::widgets::{Block, Borders, Paragraph};
|
||||||
use ratatui::Frame;
|
use ratatui::Frame;
|
||||||
@@ -7,17 +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;
|
||||||
const LABEL_COLOR: Color = Color::Rgb(120, 125, 135);
|
|
||||||
const HEADER_COLOR: Color = Color::Rgb(100, 160, 180);
|
|
||||||
const DIVIDER_COLOR: Color = Color::Rgb(60, 65, 70);
|
|
||||||
const SCROLL_INDICATOR_COLOR: Color = Color::Rgb(80, 85, 95);
|
|
||||||
|
|
||||||
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(Color::Cyan));
|
.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);
|
||||||
@@ -36,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", Color::Rgb(120, 60, 60))
|
("DISABLED", theme.link_status.disabled)
|
||||||
} else if peers > 0 {
|
} else if peers > 0 {
|
||||||
("CONNECTED", Color::Rgb(60, 120, 60))
|
("CONNECTED", theme.link_status.connected)
|
||||||
} else {
|
} else {
|
||||||
("LISTENING", Color::Rgb(120, 120, 60))
|
("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 {
|
||||||
@@ -55,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(HEADER_COLOR).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(LABEL_COLOR)),
|
Span::styled(peer_text, Style::new().fg(theme.ui.text_muted)),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Prepare values
|
// Prepare values
|
||||||
@@ -72,30 +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()
|
let tempo_style = Style::new().fg(theme.values.tempo).add_modifier(Modifier::BOLD);
|
||||||
.fg(Color::Rgb(220, 180, 100))
|
let value_style = Style::new().fg(theme.values.value);
|
||||||
.add_modifier(Modifier::BOLD);
|
|
||||||
let value_style = Style::new().fg(Color::Rgb(140, 145, 155));
|
|
||||||
|
|
||||||
// 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",
|
||||||
@@ -105,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",
|
||||||
@@ -130,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();
|
||||||
@@ -147,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)
|
||||||
@@ -178,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(SCROLL_INDICATOR_COLOR);
|
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 {
|
||||||
@@ -198,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(HEADER_COLOR).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(DIVIDER_COLOR),
|
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(Color::Yellow).add_modifier(Modifier::BOLD);
|
let highlight = Style::new().fg(theme.hint.key).add_modifier(Modifier::BOLD);
|
||||||
let normal = Style::new().fg(Color::White);
|
let normal = Style::new().fg(theme.ui.text_primary);
|
||||||
let label_style = Style::new().fg(LABEL_COLOR);
|
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 };
|
||||||
@@ -237,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(LABEL_COLOR);
|
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$}");
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use ratatui::layout::{Constraint, Layout, Rect};
|
use ratatui::layout::{Constraint, Layout, Rect};
|
||||||
use ratatui::style::{Color, Modifier, Style};
|
use ratatui::style::{Modifier, Style};
|
||||||
use ratatui::text::{Line, Span};
|
use ratatui::text::{Line, Span};
|
||||||
use ratatui::widgets::{Block, Paragraph};
|
use ratatui::widgets::{Block, Paragraph};
|
||||||
use ratatui::Frame;
|
use ratatui::Frame;
|
||||||
@@ -8,6 +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;
|
||||||
|
|
||||||
const MIN_ROW_HEIGHT: u16 = 1;
|
const MIN_ROW_HEIGHT: u16 = 1;
|
||||||
|
|
||||||
@@ -26,16 +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 {
|
let title_color = if is_focused { theme.ui.header } else { theme.ui.unfocused };
|
||||||
Color::Rgb(100, 160, 180)
|
|
||||||
} else {
|
|
||||||
Color::Rgb(70, 75, 85)
|
|
||||||
};
|
|
||||||
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);
|
||||||
@@ -94,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, _, _) => (Color::Cyan, Color::Black, ""),
|
(true, _, _) => (theme.selection.cursor, theme.selection.cursor_fg, ""),
|
||||||
(false, true, _) => (Color::Rgb(45, 80, 45), Color::Green, "> "),
|
(false, true, _) => (theme.list.playing_bg, theme.list.playing_fg, "> "),
|
||||||
(false, false, true) => (Color::Rgb(80, 60, 100), Color::Magenta, "+ "),
|
(false, false, true) => (theme.list.staged_play_bg, theme.list.staged_play_fg, "+ "),
|
||||||
(false, false, false) if is_selected => (Color::Rgb(60, 65, 75), Color::White, ""),
|
(false, false, false) if is_selected => (theme.list.hover_bg, theme.list.hover_fg, ""),
|
||||||
(false, false, false) if is_edit => (Color::Rgb(45, 106, 95), Color::White, ""),
|
(false, false, false) if is_edit => (theme.list.edit_bg, theme.list.edit_fg, ""),
|
||||||
(false, false, false) => (Color::Reset, Color::Rgb(120, 125, 135), ""),
|
(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]
|
||||||
@@ -139,7 +137,7 @@ fn render_banks(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Scroll indicators
|
// Scroll indicators
|
||||||
let indicator_style = Style::new().fg(Color::Rgb(120, 125, 135));
|
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)
|
||||||
@@ -158,16 +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 {
|
let title_color = if is_focused { theme.ui.header } else { theme.ui.unfocused };
|
||||||
Color::Rgb(100, 160, 180)
|
|
||||||
} else {
|
|
||||||
Color::Rgb(70, 75, 85)
|
|
||||||
};
|
|
||||||
|
|
||||||
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();
|
||||||
@@ -256,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, _, _, _) => (Color::Cyan, Color::Black, ""),
|
(true, _, _, _) => (theme.selection.cursor, theme.selection.cursor_fg, ""),
|
||||||
(false, true, _, true) => (Color::Rgb(120, 60, 80), Color::Magenta, "- "),
|
(false, true, _, true) => (theme.list.staged_stop_bg, theme.list.staged_stop_fg, "- "),
|
||||||
(false, true, _, false) => (Color::Rgb(45, 80, 45), Color::Green, "> "),
|
(false, true, _, false) => (theme.list.playing_bg, theme.list.playing_fg, "> "),
|
||||||
(false, false, true, _) => (Color::Rgb(80, 60, 100), Color::Magenta, "+ "),
|
(false, false, true, _) => (theme.list.staged_play_bg, theme.list.staged_play_fg, "+ "),
|
||||||
(false, false, false, _) if is_selected => (Color::Rgb(60, 65, 75), Color::White, ""),
|
(false, false, false, _) if is_selected => (theme.list.hover_bg, theme.list.hover_fg, ""),
|
||||||
(false, false, false, _) if is_edit => (Color::Rgb(45, 106, 95), Color::White, ""),
|
(false, false, false, _) if is_edit => (theme.list.edit_bg, theme.list.edit_fg, ""),
|
||||||
(false, false, false, _) => (Color::Reset, Color::Rgb(120, 125, 135), ""),
|
(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];
|
||||||
@@ -321,7 +316,7 @@ fn render_patterns(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, a
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Scroll indicators
|
// Scroll indicators
|
||||||
let indicator_style = Style::new().fg(Color::Rgb(120, 125, 135));
|
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)
|
||||||
|
|||||||
@@ -18,6 +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;
|
||||||
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,
|
||||||
@@ -131,11 +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 i = (app.ui.event_flash * app.ui.flash_brightness * 60.0) as u8;
|
let t = (app.ui.event_flash * app.ui.flash_brightness).min(1.0);
|
||||||
Color::Rgb(i, i, i)
|
let (base_r, base_g, base_b) = theme.ui.bg_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 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;
|
||||||
|
Color::Rgb(r, g, b)
|
||||||
} else {
|
} else {
|
||||||
Color::Reset
|
theme.ui.bg
|
||||||
};
|
};
|
||||||
|
|
||||||
let blank = " ".repeat(term.width as usize);
|
let blank = " ".repeat(term.width as usize);
|
||||||
@@ -253,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];
|
||||||
|
|
||||||
@@ -294,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 {
|
||||||
(Color::Rgb(30, 80, 30), " ▶ PLAYING ")
|
(theme.status.playing_bg, " ▶ PLAYING ")
|
||||||
} else {
|
} else {
|
||||||
(Color::Rgb(80, 30, 30), " ■ STOPPED ")
|
(theme.status.stopped_bg, " ■ STOPPED ")
|
||||||
};
|
};
|
||||||
let transport_style = Style::new().bg(transport_bg).fg(Color::White);
|
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)
|
||||||
@@ -308,15 +316,8 @@ fn render_header(
|
|||||||
|
|
||||||
// Fill indicator
|
// Fill indicator
|
||||||
let fill = app.live_keys.fill();
|
let fill = app.live_keys.fill();
|
||||||
let fill_style = if fill {
|
let fill_fg = if fill { theme.status.fill_on } else { theme.status.fill_off };
|
||||||
Style::new()
|
let fill_style = Style::new().bg(theme.status.fill_bg).fg(fill_fg);
|
||||||
.bg(Color::Rgb(30, 30, 35))
|
|
||||||
.fg(Color::Rgb(100, 220, 100))
|
|
||||||
} else {
|
|
||||||
Style::new()
|
|
||||||
.bg(Color::Rgb(30, 30, 35))
|
|
||||||
.fg(Color::Rgb(60, 60, 70))
|
|
||||||
};
|
|
||||||
frame.render_widget(
|
frame.render_widget(
|
||||||
Paragraph::new(if fill { "F" } else { "·" })
|
Paragraph::new(if fill { "F" } else { "·" })
|
||||||
.style(fill_style)
|
.style(fill_style)
|
||||||
@@ -326,8 +327,8 @@ fn render_header(
|
|||||||
|
|
||||||
// Tempo block
|
// Tempo block
|
||||||
let tempo_style = Style::new()
|
let tempo_style = Style::new()
|
||||||
.bg(Color::Rgb(60, 30, 60))
|
.bg(theme.header.tempo_bg)
|
||||||
.fg(Color::White)
|
.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()))
|
||||||
@@ -342,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(Color::Rgb(30, 60, 70)).fg(Color::White);
|
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)
|
||||||
@@ -373,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(Color::Rgb(30, 50, 50)).fg(Color::White);
|
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)
|
||||||
@@ -386,9 +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()
|
let stats_style = Style::new().bg(theme.header.stats_bg).fg(theme.header.stats_fg);
|
||||||
.bg(Color::Rgb(35, 35, 40))
|
|
||||||
.fg(Color::Rgb(150, 150, 160));
|
|
||||||
frame.render_widget(
|
frame.render_widget(
|
||||||
Paragraph::new(stats_text)
|
Paragraph::new(stats_text)
|
||||||
.style(stats_style)
|
.style(stats_style)
|
||||||
@@ -398,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(Color::White).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(Color::Yellow)),
|
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 {
|
||||||
@@ -480,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(Color::White).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 })),
|
||||||
];
|
];
|
||||||
@@ -488,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(Color::Yellow),
|
Style::new().fg(theme.hint.key),
|
||||||
));
|
));
|
||||||
spans.push(Span::styled(
|
spans.push(Span::styled(
|
||||||
format!(":{action}"),
|
format!(": {action}"),
|
||||||
Style::new().fg(Color::Rgb(120, 125, 135)),
|
Style::new().fg(theme.hint.text),
|
||||||
));
|
));
|
||||||
|
|
||||||
if i < n - 1 {
|
if i < n - 1 {
|
||||||
@@ -509,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 } => {
|
||||||
@@ -542,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", Color::Green),
|
FileBrowserMode::Save => ("Save As", theme.flash.success_fg),
|
||||||
FileBrowserMode::Load => ("Load From", Color::Blue),
|
FileBrowserMode::Load => ("Load From", theme.browser.directory),
|
||||||
};
|
};
|
||||||
let entries: Vec<(String, bool, bool)> = state
|
let entries: Vec<(String, bool, bool)> = state
|
||||||
.entries
|
.entries
|
||||||
@@ -561,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(Color::Magenta)
|
.border_color(theme.modal.rename)
|
||||||
.render_centered(frame, term);
|
.render_centered(frame, term);
|
||||||
}
|
}
|
||||||
Modal::RenamePattern {
|
Modal::RenamePattern {
|
||||||
@@ -574,13 +577,13 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
|
|||||||
name,
|
name,
|
||||||
)
|
)
|
||||||
.width(40)
|
.width(40)
|
||||||
.border_color(Color::Magenta)
|
.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(Color::Cyan)
|
.border_color(theme.modal.input)
|
||||||
.render_centered(frame, term);
|
.render_centered(frame, term);
|
||||||
}
|
}
|
||||||
Modal::SetPattern { field, input } => {
|
Modal::SetPattern { field, input } => {
|
||||||
@@ -591,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(Color::Yellow)
|
.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(Color::Magenta)
|
.border_color(theme.modal.rename)
|
||||||
.render_centered(frame, term);
|
.render_centered(frame, term);
|
||||||
}
|
}
|
||||||
Modal::AddSamplePath(state) => {
|
Modal::AddSamplePath(state) => {
|
||||||
@@ -611,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(Color::Magenta)
|
.border_color(theme.modal.rename)
|
||||||
.width(60)
|
.width(60)
|
||||||
.height(18)
|
.height(18)
|
||||||
.render_centered(frame, term);
|
.render_centered(frame, term);
|
||||||
@@ -636,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(Color::Rgb(120, 125, 135))
|
.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(Color::Rgb(80, 85, 95)));
|
.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,
|
||||||
@@ -698,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) => Color::Red,
|
Some(FlashKind::Error) => theme.flash.error_fg,
|
||||||
Some(FlashKind::Info) => Color::White,
|
Some(FlashKind::Info) => theme.ui.text_primary,
|
||||||
Some(FlashKind::Success) => Color::Green,
|
Some(FlashKind::Success) => theme.flash.success_fg,
|
||||||
None => Color::Rgb(100, 160, 180),
|
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()) {
|
||||||
@@ -768,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(Color::Yellow)
|
Style::default().fg(theme.search.active)
|
||||||
} else {
|
} else {
|
||||||
Style::default().fg(Color::DarkGray)
|
Style::default().fg(theme.search.inactive)
|
||||||
};
|
};
|
||||||
let cursor = if app.editor_ctx.editor.search_active() {
|
let cursor = if app.editor_ctx.editor.search_active() {
|
||||||
"_"
|
"_"
|
||||||
@@ -783,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 => Color::Rgb(60, 10, 10),
|
FlashKind::Error => theme.flash.error_bg,
|
||||||
FlashKind::Info => Color::Rgb(30, 30, 40),
|
FlashKind::Info => theme.flash.info_bg,
|
||||||
FlashKind::Success => Color::Rgb(10, 30, 10),
|
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);
|
||||||
@@ -794,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(Color::DarkGray);
|
let dim = Style::default().fg(theme.hint.text);
|
||||||
let key = Style::default().fg(Color::Yellow);
|
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![
|
||||||
@@ -863,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(Color::Cyan));
|
.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);
|
||||||
@@ -898,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(Color::Cyan)
|
.fg(theme.hint.key)
|
||||||
.add_modifier(Modifier::BOLD),
|
.add_modifier(Modifier::BOLD),
|
||||||
Style::default().fg(Color::White).bg(Color::DarkGray),
|
Style::default().fg(theme.ui.text_primary).bg(theme.ui.surface),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
(
|
(
|
||||||
Style::default().fg(Color::Gray),
|
Style::default().fg(theme.ui.text_muted),
|
||||||
Style::default().fg(Color::White),
|
Style::default().fg(theme.ui.text_primary),
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -920,17 +923,17 @@ 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::from(vec![
|
let hint_line = Line::from(vec![
|
||||||
Span::styled("↑↓", Style::default().fg(Color::Yellow)),
|
Span::styled("↑↓", Style::default().fg(theme.hint.key)),
|
||||||
Span::styled(" nav ", Style::default().fg(Color::DarkGray)),
|
Span::styled(" nav ", Style::default().fg(theme.hint.text)),
|
||||||
Span::styled("←→", Style::default().fg(Color::Yellow)),
|
Span::styled("←→", Style::default().fg(theme.hint.key)),
|
||||||
Span::styled(" change ", Style::default().fg(Color::DarkGray)),
|
Span::styled(" change ", Style::default().fg(theme.hint.text)),
|
||||||
Span::styled("Enter", Style::default().fg(Color::Yellow)),
|
Span::styled("Enter", Style::default().fg(theme.hint.key)),
|
||||||
Span::styled(" save ", Style::default().fg(Color::DarkGray)),
|
Span::styled(" save ", Style::default().fg(theme.hint.text)),
|
||||||
Span::styled("Esc", Style::default().fg(Color::Yellow)),
|
Span::styled("Esc", Style::default().fg(theme.hint.key)),
|
||||||
Span::styled(" cancel", Style::default().fg(Color::DarkGray)),
|
Span::styled(" cancel", Style::default().fg(theme.hint.text)),
|
||||||
]);
|
]);
|
||||||
frame.render_widget(Paragraph::new(hint), hint_area);
|
frame.render_widget(Paragraph::new(hint_line), hint_area);
|
||||||
}
|
}
|
||||||
Modal::KeybindingsHelp { scroll } => {
|
Modal::KeybindingsHelp { scroll } => {
|
||||||
let width = (term.width * 80 / 100).clamp(60, 100);
|
let width = (term.width * 80 / 100).clamp(60, 100);
|
||||||
@@ -940,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(Color::Rgb(100, 160, 180))
|
.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);
|
||||||
@@ -952,15 +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 {
|
let bg = if i % 2 == 0 { theme.table.row_even } else { theme.table.row_odd };
|
||||||
Color::Rgb(25, 25, 30)
|
|
||||||
} else {
|
|
||||||
Color::Rgb(35, 35, 42)
|
|
||||||
};
|
|
||||||
Row::new(vec![
|
Row::new(vec![
|
||||||
Cell::from(*key).style(Style::default().fg(Color::Yellow)),
|
Cell::from(*key).style(Style::default().fg(theme.modal.confirm)),
|
||||||
Cell::from(*name).style(Style::default().fg(Color::Cyan)),
|
Cell::from(*name).style(Style::default().fg(theme.modal.input)),
|
||||||
Cell::from(*desc).style(Style::default().fg(Color::White)),
|
Cell::from(*desc).style(Style::default().fg(theme.ui.text_primary)),
|
||||||
])
|
])
|
||||||
.style(Style::default().bg(bg))
|
.style(Style::default().bg(bg))
|
||||||
})
|
})
|
||||||
@@ -990,15 +989,15 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
|
|||||||
width: inner.width,
|
width: inner.width,
|
||||||
height: 1,
|
height: 1,
|
||||||
};
|
};
|
||||||
let hint = Line::from(vec![
|
let keybind_hint = Line::from(vec![
|
||||||
Span::styled("↑↓", Style::default().fg(Color::Yellow)),
|
Span::styled("↑↓", Style::default().fg(theme.hint.key)),
|
||||||
Span::styled(" scroll ", Style::default().fg(Color::DarkGray)),
|
Span::styled(" scroll ", Style::default().fg(theme.hint.text)),
|
||||||
Span::styled("PgUp/Dn", Style::default().fg(Color::Yellow)),
|
Span::styled("PgUp/Dn", Style::default().fg(theme.hint.key)),
|
||||||
Span::styled(" page ", Style::default().fg(Color::DarkGray)),
|
Span::styled(" page ", Style::default().fg(theme.hint.text)),
|
||||||
Span::styled("Esc/?", Style::default().fg(Color::Yellow)),
|
Span::styled("Esc/?", Style::default().fg(theme.hint.key)),
|
||||||
Span::styled(" close", Style::default().fg(Color::DarkGray)),
|
Span::styled(" close", Style::default().fg(theme.hint.text)),
|
||||||
]);
|
]);
|
||||||
frame.render_widget(Paragraph::new(hint).alignment(Alignment::Right), hint_area);
|
frame.render_widget(Paragraph::new(keybind_hint).alignment(Alignment::Right), hint_area);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,24 @@
|
|||||||
use ratatui::layout::{Alignment, Constraint, Layout, Rect};
|
use ratatui::layout::{Alignment, Constraint, Layout, Rect};
|
||||||
use ratatui::style::{Color, Style, Stylize};
|
use ratatui::style::Style;
|
||||||
use ratatui::text::{Line, Span};
|
use ratatui::text::{Line, Span};
|
||||||
use ratatui::widgets::Paragraph;
|
use ratatui::widgets::Paragraph;
|
||||||
use ratatui::Frame;
|
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;
|
||||||
|
|
||||||
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(Color::Rgb(180, 140, 200));
|
let author_style = Style::new().fg(theme.title.author);
|
||||||
let link_style = Style::new().fg(Color::Rgb(120, 200, 180));
|
let link_style = Style::new().fg(theme.title.link);
|
||||||
let license_style = Style::new().fg(Color::Rgb(200, 160, 100));
|
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().cyan().bold())
|
.style(Style::new().fg(theme.title.big_title).bold())
|
||||||
.lines(vec!["CAGIRE".into()])
|
.lines(vec!["CAGIRE".into()])
|
||||||
.centered()
|
.centered()
|
||||||
.build();
|
.build();
|
||||||
@@ -25,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(Color::White),
|
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)),
|
||||||
@@ -37,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(Color::Rgb(140, 160, 170)),
|
Style::new().fg(theme.title.prompt),
|
||||||
)),
|
)),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -140,7 +140,7 @@
|
|||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<a href="https://github.com/Bubobubobubobubo/cagire/releases/latest/download/cagire-macos-aarch64" class="btn">macOS (ARM)</a>
|
<a href="https://github.com/Bubobubobubobubo/cagire/releases/latest/download/cagire-macos-aarch64" class="btn">macOS (ARM)</a>
|
||||||
<a href="https://github.com/Bubobubobubobubo/cagire/releases/latest/download/cagire-macos-x86_64" class="btn">macOS (Intel)</a>
|
<a href="https://github.com/Bubobubobubobubo/cagire/releases/latest/download/cagire-macos-x86_64" class="btn">macOS (Intel)</a>
|
||||||
<a href="https://github.com/Bubobubobubobubo/cagire/releases/latest/download/cagire-windows-x86_64" class="btn">Windows</a>
|
<a href="https://github.com/Bubobubobubobubo/cagire/releases/latest/download/cagire-windows-x86_64.exe" class="btn">Windows</a>
|
||||||
<a href="https://github.com/Bubobubobubobubo/cagire/releases/latest/download/cagire-linux-x86_64" class="btn">Linux</a>
|
<a href="https://github.com/Bubobubobubobubo/cagire/releases/latest/download/cagire-linux-x86_64" class="btn">Linux</a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
Reference in New Issue
Block a user