Compare commits
2 Commits
12b90bc99b
...
302f40c4ac
| Author | SHA1 | Date | |
|---|---|---|---|
| 302f40c4ac | |||
| 79a4c3b6e2 |
@@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.
|
|||||||
|
|
||||||
## [0.1.0]
|
## [0.1.0]
|
||||||
|
|
||||||
|
### UI / UX (breaking cosmetic changes)
|
||||||
|
- **Options page**: Each option now shows a short description line below when focused, replacing the static header box.
|
||||||
|
- **Dictionary page**: Removed the Forth description box at the top. The word list now uses the full page height.
|
||||||
|
|
||||||
### CLAP Plugin (experimental)
|
### CLAP Plugin (experimental)
|
||||||
- Early CLAP plugin support via nih-plug, baseview, and egui. Feature-gated builds separate CLI from plugin targets.
|
- Early CLAP plugin support via nih-plug, baseview, and egui. Feature-gated builds separate CLI from plugin targets.
|
||||||
|
|
||||||
|
|||||||
@@ -1,104 +0,0 @@
|
|||||||
use crate::theme;
|
|
||||||
use ratatui::buffer::Buffer;
|
|
||||||
use ratatui::layout::Rect;
|
|
||||||
use ratatui::widgets::Widget;
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub enum MuteStatus {
|
|
||||||
Normal,
|
|
||||||
Muted,
|
|
||||||
Soloed,
|
|
||||||
EffectivelyMuted, // Solo active on another pattern
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct ActivePatterns<'a> {
|
|
||||||
patterns: &'a [(usize, usize, usize)], // (bank, pattern, iter)
|
|
||||||
mute_status: Option<&'a [MuteStatus]>,
|
|
||||||
current_step: Option<(usize, usize)>, // (current_step, total_steps)
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> ActivePatterns<'a> {
|
|
||||||
pub fn new(patterns: &'a [(usize, usize, usize)]) -> Self {
|
|
||||||
Self {
|
|
||||||
patterns,
|
|
||||||
mute_status: None,
|
|
||||||
current_step: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_step(mut self, current: usize, total: usize) -> Self {
|
|
||||||
self.current_step = Some((current, total));
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_mute_status(mut self, status: &'a [MuteStatus]) -> Self {
|
|
||||||
self.mute_status = Some(status);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Widget for ActivePatterns<'_> {
|
|
||||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
|
||||||
if area.width < 10 || area.height == 0 {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let theme = theme::get();
|
|
||||||
|
|
||||||
let max_pattern_rows = if self.current_step.is_some() {
|
|
||||||
area.height.saturating_sub(1) as usize
|
|
||||||
} else {
|
|
||||||
area.height as usize
|
|
||||||
};
|
|
||||||
|
|
||||||
for (row, &(bank, pattern, iter)) in self.patterns.iter().enumerate() {
|
|
||||||
if row >= max_pattern_rows {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mute_status = self
|
|
||||||
.mute_status
|
|
||||||
.and_then(|s| s.get(row))
|
|
||||||
.copied()
|
|
||||||
.unwrap_or(MuteStatus::Normal);
|
|
||||||
|
|
||||||
let (prefix, fg, bg) = match mute_status {
|
|
||||||
MuteStatus::Soloed => ("S", theme.list.soloed_fg, theme.list.soloed_bg),
|
|
||||||
MuteStatus::Muted => ("M", theme.list.muted_fg, theme.list.muted_bg),
|
|
||||||
MuteStatus::EffectivelyMuted => (" ", theme.list.muted_fg, theme.list.muted_bg),
|
|
||||||
MuteStatus::Normal => {
|
|
||||||
let bg = if row % 2 == 0 {
|
|
||||||
theme.table.row_even
|
|
||||||
} else {
|
|
||||||
theme.table.row_odd
|
|
||||||
};
|
|
||||||
(" ", theme.ui.text_primary, bg)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let text = format!("{}B{:02}:{:02}({:02})", prefix, bank + 1, pattern + 1, iter.min(99));
|
|
||||||
let y = area.y + row as u16;
|
|
||||||
|
|
||||||
let mut chars = text.chars();
|
|
||||||
for col in 0..area.width as usize {
|
|
||||||
let ch = chars.next().unwrap_or(' ');
|
|
||||||
buf[(area.x + col as u16, y)]
|
|
||||||
.set_char(ch)
|
|
||||||
.set_fg(fg)
|
|
||||||
.set_bg(bg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some((current, total)) = self.current_step {
|
|
||||||
let text = format!("{:02}/{:02}", current + 1, total);
|
|
||||||
let y = area.y + area.height.saturating_sub(1);
|
|
||||||
let mut chars = text.chars();
|
|
||||||
for col in 0..area.width as usize {
|
|
||||||
let ch = chars.next().unwrap_or(' ');
|
|
||||||
buf[(area.x + col as u16, y)]
|
|
||||||
.set_char(ch)
|
|
||||||
.set_fg(theme.ui.text_primary)
|
|
||||||
.set_bg(theme.table.row_even);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
mod active_patterns;
|
|
||||||
mod category_list;
|
mod category_list;
|
||||||
mod confirm;
|
mod confirm;
|
||||||
mod editor;
|
mod editor;
|
||||||
@@ -20,7 +19,6 @@ pub mod theme;
|
|||||||
mod vu_meter;
|
mod vu_meter;
|
||||||
mod waveform;
|
mod waveform;
|
||||||
|
|
||||||
pub use active_patterns::{ActivePatterns, MuteStatus};
|
|
||||||
pub use category_list::{CategoryItem, CategoryList, Selection};
|
pub use category_list::{CategoryItem, CategoryList, Selection};
|
||||||
pub use confirm::ConfirmModal;
|
pub use confirm::ConfirmModal;
|
||||||
pub use editor::{fuzzy_match, CompletionCandidate, Editor};
|
pub use editor::{fuzzy_match, CompletionCandidate, Editor};
|
||||||
|
|||||||
@@ -388,6 +388,7 @@ impl App {
|
|||||||
AppCommand::ToggleScope => self.audio.config.show_scope = !self.audio.config.show_scope,
|
AppCommand::ToggleScope => self.audio.config.show_scope = !self.audio.config.show_scope,
|
||||||
AppCommand::ToggleSpectrum => self.audio.config.show_spectrum = !self.audio.config.show_spectrum,
|
AppCommand::ToggleSpectrum => self.audio.config.show_spectrum = !self.audio.config.show_spectrum,
|
||||||
AppCommand::TogglePreview => self.audio.config.show_preview = !self.audio.config.show_preview,
|
AppCommand::TogglePreview => self.audio.config.show_preview = !self.audio.config.show_preview,
|
||||||
|
AppCommand::TogglePerformanceMode => self.ui.performance_mode = !self.ui.performance_mode,
|
||||||
|
|
||||||
// Metrics
|
// Metrics
|
||||||
AppCommand::ResetPeakVoices => self.metrics.peak_voices = 0,
|
AppCommand::ResetPeakVoices => self.metrics.peak_voices = 0,
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ impl App {
|
|||||||
show_spectrum: self.audio.config.show_spectrum,
|
show_spectrum: self.audio.config.show_spectrum,
|
||||||
show_preview: self.audio.config.show_preview,
|
show_preview: self.audio.config.show_preview,
|
||||||
show_completion: self.ui.show_completion,
|
show_completion: self.ui.show_completion,
|
||||||
|
performance_mode: self.ui.performance_mode,
|
||||||
color_scheme: self.ui.color_scheme,
|
color_scheme: self.ui.color_scheme,
|
||||||
layout: self.audio.config.layout,
|
layout: self.audio.config.layout,
|
||||||
hue_rotation: self.ui.hue_rotation,
|
hue_rotation: self.ui.hue_rotation,
|
||||||
|
|||||||
@@ -251,6 +251,7 @@ pub enum AppCommand {
|
|||||||
ToggleScope,
|
ToggleScope,
|
||||||
ToggleSpectrum,
|
ToggleSpectrum,
|
||||||
TogglePreview,
|
TogglePreview,
|
||||||
|
TogglePerformanceMode,
|
||||||
|
|
||||||
// Metrics
|
// Metrics
|
||||||
ResetPeakVoices,
|
ResetPeakVoices,
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ pub fn init(args: InitArgs) -> Init {
|
|||||||
app.audio.config.show_spectrum = settings.display.show_spectrum;
|
app.audio.config.show_spectrum = settings.display.show_spectrum;
|
||||||
app.audio.config.show_preview = settings.display.show_preview;
|
app.audio.config.show_preview = settings.display.show_preview;
|
||||||
app.ui.show_completion = settings.display.show_completion;
|
app.ui.show_completion = settings.display.show_completion;
|
||||||
|
app.ui.performance_mode = settings.display.performance_mode;
|
||||||
app.ui.color_scheme = settings.display.color_scheme;
|
app.ui.color_scheme = settings.display.color_scheme;
|
||||||
app.ui.hue_rotation = settings.display.hue_rotation;
|
app.ui.hue_rotation = settings.display.hue_rotation;
|
||||||
app.audio.config.layout = settings.display.layout;
|
app.audio.config.layout = settings.display.layout;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use ratatui::layout::{Constraint, Layout, Rect};
|
|||||||
use crate::commands::AppCommand;
|
use crate::commands::AppCommand;
|
||||||
use crate::page::Page;
|
use crate::page::Page;
|
||||||
use crate::state::{
|
use crate::state::{
|
||||||
DeviceKind, DictFocus, EngineSection, HelpFocus, MainLayout, MinimapMode, Modal, OptionsFocus,
|
DeviceKind, DictFocus, EngineSection, HelpFocus, MinimapMode, Modal, OptionsFocus,
|
||||||
PatternsColumn, SettingKind,
|
PatternsColumn, SettingKind,
|
||||||
};
|
};
|
||||||
use crate::views::{dict_view, engine_view, help_view, main_view, patterns_view};
|
use crate::views::{dict_view, engine_view, help_view, main_view, patterns_view};
|
||||||
@@ -35,10 +35,11 @@ pub fn handle_mouse(ctx: &mut InputContext, mouse: MouseEvent, term: Rect) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn padded(term: Rect) -> Rect {
|
fn padded(term: Rect) -> Rect {
|
||||||
|
let h_pad = crate::views::horizontal_padding(term.width);
|
||||||
Rect {
|
Rect {
|
||||||
x: term.x + 4,
|
x: term.x + h_pad,
|
||||||
y: term.y + 1,
|
y: term.y + 1,
|
||||||
width: term.width.saturating_sub(8),
|
width: term.width.saturating_sub(h_pad * 2),
|
||||||
height: term.height.saturating_sub(2),
|
height: term.height.saturating_sub(2),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -139,10 +140,7 @@ fn handle_scroll(ctx: &mut InputContext, col: u16, row: u16, term: Rect, up: boo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Page::Dict => {
|
Page::Dict => {
|
||||||
let (header_area, [cat_area, words_area]) = dict_view::layout(body);
|
let [cat_area, words_area] = dict_view::layout(body);
|
||||||
if contains(header_area, col, row) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if contains(cat_area, col, row) {
|
if contains(cat_area, col, row) {
|
||||||
if up {
|
if up {
|
||||||
ctx.dispatch(AppCommand::DictPrevCategory);
|
ctx.dispatch(AppCommand::DictPrevCategory);
|
||||||
@@ -300,120 +298,44 @@ fn handle_body_click(ctx: &mut InputContext, col: u16, row: u16, body: Rect) {
|
|||||||
// --- Main page (grid) ---
|
// --- Main page (grid) ---
|
||||||
|
|
||||||
fn handle_main_click(ctx: &mut InputContext, col: u16, row: u16, area: Rect) {
|
fn handle_main_click(ctx: &mut InputContext, col: u16, row: u16, area: Rect) {
|
||||||
let [_patterns_area, _, main_area, _, _vu_area] = main_view::layout(area);
|
let [main_area, _, _vu_area] = main_view::layout(area);
|
||||||
|
|
||||||
if !contains(main_area, col, row) {
|
if !contains(main_area, col, row) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Replay viz/sequencer split
|
let pattern = ctx.app.current_edit_pattern();
|
||||||
let show_scope = ctx.app.audio.config.show_scope;
|
let has_viz = ctx.app.audio.config.show_scope
|
||||||
let show_spectrum = ctx.app.audio.config.show_spectrum;
|
|| ctx.app.audio.config.show_spectrum
|
||||||
let show_preview = ctx.app.audio.config.show_preview;
|
|| ctx.app.audio.config.show_preview;
|
||||||
let has_viz = show_scope || show_spectrum || show_preview;
|
let seq_h = main_view::sequencer_height(pattern.length, ctx.app.editor_ctx.step);
|
||||||
let layout = ctx.app.audio.config.layout;
|
let (_, sequencer_area) =
|
||||||
|
main_view::viz_seq_split(main_area, ctx.app.audio.config.layout, has_viz, seq_h);
|
||||||
let sequencer_area = match layout {
|
|
||||||
MainLayout::Top => {
|
|
||||||
let viz_height = if has_viz { 16 } else { 0 };
|
|
||||||
let [_viz, seq] =
|
|
||||||
Layout::vertical([Constraint::Length(viz_height), Constraint::Fill(1)])
|
|
||||||
.areas(main_area);
|
|
||||||
seq
|
|
||||||
}
|
|
||||||
MainLayout::Bottom => {
|
|
||||||
let viz_height = if has_viz { 16 } else { 0 };
|
|
||||||
let [seq, _viz] =
|
|
||||||
Layout::vertical([Constraint::Fill(1), Constraint::Length(viz_height)])
|
|
||||||
.areas(main_area);
|
|
||||||
seq
|
|
||||||
}
|
|
||||||
MainLayout::Left => {
|
|
||||||
let viz_width = if has_viz { 33 } else { 0 };
|
|
||||||
let [_viz, _spacer, seq] = Layout::horizontal([
|
|
||||||
Constraint::Percentage(viz_width),
|
|
||||||
Constraint::Length(2),
|
|
||||||
Constraint::Fill(1),
|
|
||||||
])
|
|
||||||
.areas(main_area);
|
|
||||||
seq
|
|
||||||
}
|
|
||||||
MainLayout::Right => {
|
|
||||||
let viz_width = if has_viz { 33 } else { 0 };
|
|
||||||
let [seq, _spacer, _viz] = Layout::horizontal([
|
|
||||||
Constraint::Fill(1),
|
|
||||||
Constraint::Length(2),
|
|
||||||
Constraint::Percentage(viz_width),
|
|
||||||
])
|
|
||||||
.areas(main_area);
|
|
||||||
seq
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if !contains(sequencer_area, col, row) {
|
if !contains(sequencer_area, col, row) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Replay grid layout to find which step was clicked
|
|
||||||
if let Some(step) = hit_test_grid(ctx, col, row, sequencer_area) {
|
if let Some(step) = hit_test_grid(ctx, col, row, sequencer_area) {
|
||||||
ctx.dispatch(AppCommand::GoToStep(step));
|
ctx.dispatch(AppCommand::GoToStep(step));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn hit_test_grid(ctx: &InputContext, col: u16, row: u16, area: Rect) -> Option<usize> {
|
fn hit_test_grid(ctx: &InputContext, col: u16, row: u16, area: Rect) -> Option<usize> {
|
||||||
if area.width < 50 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let pattern = ctx.app.current_edit_pattern();
|
let pattern = ctx.app.current_edit_pattern();
|
||||||
let length = pattern.length;
|
let length = pattern.length;
|
||||||
let page = ctx.app.editor_ctx.step / STEPS_PER_PAGE;
|
let page = ctx.app.editor_ctx.step / STEPS_PER_PAGE;
|
||||||
let page_start = page * STEPS_PER_PAGE;
|
let page_start = page * STEPS_PER_PAGE;
|
||||||
let steps_on_page = (page_start + STEPS_PER_PAGE).min(length) - page_start;
|
let steps_on_page = (page_start + STEPS_PER_PAGE).min(length) - page_start;
|
||||||
|
|
||||||
let num_rows = steps_on_page.div_ceil(8);
|
for (tile_rect, step_offset) in main_view::grid_layout(area, steps_on_page) {
|
||||||
let steps_per_row = steps_on_page.div_ceil(num_rows);
|
if contains(tile_rect, col, row) {
|
||||||
|
let step_idx = page_start + step_offset;
|
||||||
let row_height = area.height / num_rows as u16;
|
|
||||||
|
|
||||||
let row_constraints: Vec<Constraint> = (0..num_rows)
|
|
||||||
.map(|_| Constraint::Length(row_height))
|
|
||||||
.collect();
|
|
||||||
let rows = Layout::vertical(row_constraints).split(area);
|
|
||||||
|
|
||||||
for row_idx in 0..num_rows {
|
|
||||||
let row_area = rows[row_idx];
|
|
||||||
if !contains(row_area, col, row) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let start_step = row_idx * steps_per_row;
|
|
||||||
let end_step = (start_step + steps_per_row).min(steps_on_page);
|
|
||||||
let cols_in_row = end_step - start_step;
|
|
||||||
|
|
||||||
let col_constraints: Vec<Constraint> = (0..cols_in_row * 2 - 1)
|
|
||||||
.map(|i| {
|
|
||||||
if i % 2 == 0 {
|
|
||||||
Constraint::Fill(1)
|
|
||||||
} else if i == cols_in_row - 1 {
|
|
||||||
Constraint::Length(2)
|
|
||||||
} else {
|
|
||||||
Constraint::Length(1)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
let cols = Layout::horizontal(col_constraints).split(row_area);
|
|
||||||
|
|
||||||
for col_idx in 0..cols_in_row {
|
|
||||||
let tile_area = cols[col_idx * 2];
|
|
||||||
if contains(tile_area, col, row) {
|
|
||||||
let step_idx = page_start + start_step + col_idx;
|
|
||||||
if step_idx < length {
|
if step_idx < length {
|
||||||
return Some(step_idx);
|
return Some(step_idx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
@@ -533,7 +455,7 @@ fn handle_help_click(ctx: &mut InputContext, col: u16, row: u16, area: Rect) {
|
|||||||
// --- Dict page ---
|
// --- Dict page ---
|
||||||
|
|
||||||
fn handle_dict_click(ctx: &mut InputContext, col: u16, row: u16, area: Rect) {
|
fn handle_dict_click(ctx: &mut InputContext, col: u16, row: u16, area: Rect) {
|
||||||
let (_header_area, [cat_area, words_area]) = dict_view::layout(area);
|
let [cat_area, words_area] = dict_view::layout(area);
|
||||||
|
|
||||||
if contains(cat_area, col, row) {
|
if contains(cat_area, col, row) {
|
||||||
use crate::model::categories::{self, CatEntry, CATEGORIES};
|
use crate::model::categories::{self, CatEntry, CATEGORIES};
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ pub(crate) fn cycle_option_value(ctx: &mut InputContext, right: bool) {
|
|||||||
OptionsFocus::ShowSpectrum => ctx.dispatch(AppCommand::ToggleSpectrum),
|
OptionsFocus::ShowSpectrum => ctx.dispatch(AppCommand::ToggleSpectrum),
|
||||||
OptionsFocus::ShowCompletion => ctx.dispatch(AppCommand::ToggleCompletion),
|
OptionsFocus::ShowCompletion => ctx.dispatch(AppCommand::ToggleCompletion),
|
||||||
OptionsFocus::ShowPreview => ctx.dispatch(AppCommand::TogglePreview),
|
OptionsFocus::ShowPreview => ctx.dispatch(AppCommand::TogglePreview),
|
||||||
|
OptionsFocus::PerformanceMode => ctx.dispatch(AppCommand::TogglePerformanceMode),
|
||||||
OptionsFocus::Font => {
|
OptionsFocus::Font => {
|
||||||
const FONTS: &[&str] = &["6x13", "7x13", "8x13", "9x15", "9x18", "10x20"];
|
const FONTS: &[&str] = &["6x13", "7x13", "8x13", "9x15", "9x18", "10x20"];
|
||||||
let pos = FONTS.iter().position(|f| *f == ctx.app.ui.font).unwrap_or(2);
|
let pos = FONTS.iter().position(|f| *f == ctx.app.ui.font).unwrap_or(2);
|
||||||
|
|||||||
@@ -53,6 +53,8 @@ pub struct DisplaySettings {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub layout: MainLayout,
|
pub layout: MainLayout,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
pub performance_mode: bool,
|
||||||
|
#[serde(default)]
|
||||||
pub hue_rotation: f32,
|
pub hue_rotation: f32,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub onboarding_dismissed: Vec<String>,
|
pub onboarding_dismissed: Vec<String>,
|
||||||
@@ -98,6 +100,7 @@ impl Default for DisplaySettings {
|
|||||||
show_completion: true,
|
show_completion: true,
|
||||||
font: default_font(),
|
font: default_font(),
|
||||||
zoom_factor: default_zoom(),
|
zoom_factor: default_zoom(),
|
||||||
|
performance_mode: false,
|
||||||
color_scheme: ColorScheme::default(),
|
color_scheme: ColorScheme::default(),
|
||||||
layout: MainLayout::default(),
|
layout: MainLayout::default(),
|
||||||
hue_rotation: 0.0,
|
hue_rotation: 0.0,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ pub enum OptionsFocus {
|
|||||||
ShowSpectrum,
|
ShowSpectrum,
|
||||||
ShowCompletion,
|
ShowCompletion,
|
||||||
ShowPreview,
|
ShowPreview,
|
||||||
|
PerformanceMode,
|
||||||
Font,
|
Font,
|
||||||
ZoomFactor,
|
ZoomFactor,
|
||||||
WindowSize,
|
WindowSize,
|
||||||
@@ -38,6 +39,7 @@ impl CyclicEnum for OptionsFocus {
|
|||||||
Self::ShowSpectrum,
|
Self::ShowSpectrum,
|
||||||
Self::ShowCompletion,
|
Self::ShowCompletion,
|
||||||
Self::ShowPreview,
|
Self::ShowPreview,
|
||||||
|
Self::PerformanceMode,
|
||||||
Self::Font,
|
Self::Font,
|
||||||
Self::ZoomFactor,
|
Self::ZoomFactor,
|
||||||
Self::WindowSize,
|
Self::WindowSize,
|
||||||
@@ -90,26 +92,27 @@ const FULL_LAYOUT: &[(OptionsFocus, usize)] = &[
|
|||||||
(OptionsFocus::ShowSpectrum, 7),
|
(OptionsFocus::ShowSpectrum, 7),
|
||||||
(OptionsFocus::ShowCompletion, 8),
|
(OptionsFocus::ShowCompletion, 8),
|
||||||
(OptionsFocus::ShowPreview, 9),
|
(OptionsFocus::ShowPreview, 9),
|
||||||
(OptionsFocus::Font, 10),
|
(OptionsFocus::PerformanceMode, 10),
|
||||||
(OptionsFocus::ZoomFactor, 11),
|
(OptionsFocus::Font, 11),
|
||||||
(OptionsFocus::WindowSize, 12),
|
(OptionsFocus::ZoomFactor, 12),
|
||||||
// blank=13, ABLETON LINK header=14, divider=15
|
(OptionsFocus::WindowSize, 13),
|
||||||
(OptionsFocus::LinkEnabled, 16),
|
// blank=14, ABLETON LINK header=15, divider=16
|
||||||
(OptionsFocus::StartStopSync, 17),
|
(OptionsFocus::LinkEnabled, 17),
|
||||||
(OptionsFocus::Quantum, 18),
|
(OptionsFocus::StartStopSync, 18),
|
||||||
// blank=19, SESSION header=20, divider=21, Tempo=22, Beat=23, Phase=24
|
(OptionsFocus::Quantum, 19),
|
||||||
// blank=25, MIDI OUTPUTS header=26, divider=27
|
// blank=20, SESSION header=21, divider=22, Tempo=23, Beat=24, Phase=25
|
||||||
(OptionsFocus::MidiOutput0, 28),
|
// blank=26, MIDI OUTPUTS header=27, divider=28
|
||||||
(OptionsFocus::MidiOutput1, 29),
|
(OptionsFocus::MidiOutput0, 29),
|
||||||
(OptionsFocus::MidiOutput2, 30),
|
(OptionsFocus::MidiOutput1, 30),
|
||||||
(OptionsFocus::MidiOutput3, 31),
|
(OptionsFocus::MidiOutput2, 31),
|
||||||
// blank=32, MIDI INPUTS header=33, divider=34
|
(OptionsFocus::MidiOutput3, 32),
|
||||||
(OptionsFocus::MidiInput0, 35),
|
// blank=33, MIDI INPUTS header=34, divider=35
|
||||||
(OptionsFocus::MidiInput1, 36),
|
(OptionsFocus::MidiInput0, 36),
|
||||||
(OptionsFocus::MidiInput2, 37),
|
(OptionsFocus::MidiInput1, 37),
|
||||||
(OptionsFocus::MidiInput3, 38),
|
(OptionsFocus::MidiInput2, 38),
|
||||||
// blank=39, ONBOARDING header=40, divider=41
|
(OptionsFocus::MidiInput3, 39),
|
||||||
(OptionsFocus::ResetOnboarding, 42),
|
// blank=40, ONBOARDING header=41, divider=42
|
||||||
|
(OptionsFocus::ResetOnboarding, 43),
|
||||||
];
|
];
|
||||||
|
|
||||||
impl OptionsFocus {
|
impl OptionsFocus {
|
||||||
@@ -165,17 +168,14 @@ fn visible_layout(plugin_mode: bool) -> Vec<(OptionsFocus, usize)> {
|
|||||||
// based on which sections are hidden.
|
// based on which sections are hidden.
|
||||||
let mut offset: usize = 0;
|
let mut offset: usize = 0;
|
||||||
|
|
||||||
// Font/Zoom/Window lines (10,11,12) hidden when !plugin_mode
|
// Font/Zoom/Window lines (11,12,13) hidden when !plugin_mode
|
||||||
if !plugin_mode {
|
if !plugin_mode {
|
||||||
offset += 3; // 3 lines for Font, ZoomFactor, WindowSize
|
offset += 3; // 3 lines for Font, ZoomFactor, WindowSize
|
||||||
}
|
}
|
||||||
|
|
||||||
// Link + Session + MIDI sections hidden when plugin_mode
|
// Link + Session + MIDI sections hidden when plugin_mode
|
||||||
// These span from blank(13) through MidiInput3(38) = 26 lines
|
// These span from blank(14) through MidiInput3(39) = 26 lines
|
||||||
if plugin_mode {
|
if plugin_mode {
|
||||||
// blank + LINK header + divider + 3 options + blank + SESSION header + divider + 3 readonlys
|
|
||||||
// + blank + MIDI OUT header + divider + 4 options + blank + MIDI IN header + divider + 4 options
|
|
||||||
// = 26 lines (indices 13..=38)
|
|
||||||
let link_section_lines = 26;
|
let link_section_lines = 26;
|
||||||
offset += link_section_lines;
|
offset += link_section_lines;
|
||||||
}
|
}
|
||||||
@@ -185,10 +185,10 @@ fn visible_layout(plugin_mode: bool) -> Vec<(OptionsFocus, usize)> {
|
|||||||
if !focus.is_visible(plugin_mode) {
|
if !focus.is_visible(plugin_mode) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// Lines at or below index 9 (ShowPreview) are never shifted
|
// Lines at or below index 10 (PerformanceMode) are never shifted
|
||||||
let adjusted = if raw_line <= 9 {
|
let adjusted = if raw_line <= 10 {
|
||||||
raw_line
|
raw_line
|
||||||
} else if !plugin_mode && raw_line <= 12 {
|
} else if !plugin_mode && raw_line <= 13 {
|
||||||
// Font/Zoom/Window — these are hidden, skip
|
// Font/Zoom/Window — these are hidden, skip
|
||||||
continue;
|
continue;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ pub struct UiState {
|
|||||||
pub prev_page: Page,
|
pub prev_page: Page,
|
||||||
pub prev_show_title: bool,
|
pub prev_show_title: bool,
|
||||||
pub onboarding_dismissed: Vec<String>,
|
pub onboarding_dismissed: Vec<String>,
|
||||||
|
pub performance_mode: bool,
|
||||||
pub font: String,
|
pub font: String,
|
||||||
pub zoom_factor: f32,
|
pub zoom_factor: f32,
|
||||||
pub window_width: u32,
|
pub window_width: u32,
|
||||||
@@ -121,6 +122,7 @@ impl Default for UiState {
|
|||||||
prev_page: Page::default(),
|
prev_page: Page::default(),
|
||||||
prev_show_title: true,
|
prev_show_title: true,
|
||||||
onboarding_dismissed: Vec::new(),
|
onboarding_dismissed: Vec::new(),
|
||||||
|
performance_mode: false,
|
||||||
font: "8x13".to_string(),
|
font: "8x13".to_string(),
|
||||||
zoom_factor: 1.5,
|
zoom_factor: 1.5,
|
||||||
window_width: 1200,
|
window_width: 1200,
|
||||||
|
|||||||
@@ -13,40 +13,18 @@ use crate::widgets::{render_search_bar, CategoryItem, CategoryList, Selection};
|
|||||||
|
|
||||||
use CatEntry::{Category, Section};
|
use CatEntry::{Category, Section};
|
||||||
|
|
||||||
pub fn layout(area: Rect) -> (Rect, [Rect; 2]) {
|
pub fn layout(area: Rect) -> [Rect; 2] {
|
||||||
let [header_area, body_area] =
|
Layout::horizontal([Constraint::Length(16), Constraint::Fill(1)]).areas(area)
|
||||||
Layout::vertical([Constraint::Length(5), Constraint::Fill(1)]).areas(area);
|
|
||||||
let body = Layout::horizontal([Constraint::Length(16), Constraint::Fill(1)]).areas(body_area);
|
|
||||||
(header_area, body)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render(frame: &mut Frame, app: &App, area: Rect) {
|
pub fn render(frame: &mut Frame, app: &App, area: Rect) {
|
||||||
let (header_area, [cat_area, words_area]) = layout(area);
|
let [cat_area, words_area] = layout(area);
|
||||||
|
|
||||||
render_header(frame, header_area);
|
|
||||||
|
|
||||||
let is_searching = !app.ui.dict_search_query.is_empty();
|
let is_searching = !app.ui.dict_search_query.is_empty();
|
||||||
render_categories(frame, app, cat_area, is_searching);
|
render_categories(frame, app, cat_area, is_searching);
|
||||||
render_words(frame, app, words_area, is_searching);
|
render_words(frame, app, words_area, is_searching);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_header(frame: &mut Frame, area: Rect) {
|
|
||||||
use ratatui::widgets::Wrap;
|
|
||||||
let theme = theme::get();
|
|
||||||
let desc = "Forth uses a stack: values are pushed, functions (called words) consume and \
|
|
||||||
produce values. Read left to right: 3 4 + -> push 3, push 4, + pops both, \
|
|
||||||
pushes 7. This page lists all words with their signature ( inputs -- outputs ).";
|
|
||||||
let block = Block::default()
|
|
||||||
.borders(Borders::ALL)
|
|
||||||
.border_style(Style::new().fg(theme.dict.border_normal))
|
|
||||||
.title("Dictionary");
|
|
||||||
let para = Paragraph::new(desc)
|
|
||||||
.style(Style::new().fg(theme.dict.header_desc))
|
|
||||||
.wrap(Wrap { trim: false })
|
|
||||||
.block(block);
|
|
||||||
frame.render_widget(para, area);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_categories(frame: &mut Frame, app: &App, area: Rect, dimmed: bool) {
|
fn render_categories(frame: &mut Frame, app: &App, area: Rect, dimmed: bool) {
|
||||||
let theme = theme::get();
|
let theme = theme::get();
|
||||||
let focused = app.ui.dict_focus == DictFocus::Categories && !dimmed;
|
let focused = app.ui.dict_focus == DictFocus::Categories && !dimmed;
|
||||||
|
|||||||
@@ -13,12 +13,10 @@ use crate::state::MainLayout;
|
|||||||
use crate::theme;
|
use crate::theme;
|
||||||
use crate::views::highlight::highlight_line_with_runtime;
|
use crate::views::highlight::highlight_line_with_runtime;
|
||||||
use crate::views::render::{adjust_resolved_for_line, adjust_spans_for_line};
|
use crate::views::render::{adjust_resolved_for_line, adjust_spans_for_line};
|
||||||
use crate::widgets::{ActivePatterns, Orientation, Scope, Spectrum, VuMeter};
|
use crate::widgets::{Orientation, Scope, Spectrum, VuMeter};
|
||||||
|
|
||||||
pub fn layout(area: Rect) -> [Rect; 5] {
|
pub fn layout(area: Rect) -> [Rect; 3] {
|
||||||
Layout::horizontal([
|
Layout::horizontal([
|
||||||
Constraint::Length(13),
|
|
||||||
Constraint::Length(2),
|
|
||||||
Constraint::Fill(1),
|
Constraint::Fill(1),
|
||||||
Constraint::Length(2),
|
Constraint::Length(2),
|
||||||
Constraint::Length(10),
|
Constraint::Length(10),
|
||||||
@@ -27,54 +25,13 @@ pub fn layout(area: Rect) -> [Rect; 5] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn render(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area: Rect) {
|
pub fn render(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area: Rect) {
|
||||||
let [patterns_area, _, main_area, _, vu_area] = layout(area);
|
let [main_area, _, vu_area] = layout(area);
|
||||||
|
|
||||||
let show_scope = app.audio.config.show_scope;
|
let has_viz = app.audio.config.show_scope
|
||||||
let show_spectrum = app.audio.config.show_spectrum;
|
|| app.audio.config.show_spectrum
|
||||||
let show_preview = app.audio.config.show_preview;
|
|| app.audio.config.show_preview;
|
||||||
let has_viz = show_scope || show_spectrum || show_preview;
|
let seq_h = sequencer_height(app.current_edit_pattern().length, app.editor_ctx.step);
|
||||||
let layout = app.audio.config.layout;
|
let (viz_area, sequencer_area) = viz_seq_split(main_area, app.audio.config.layout, has_viz, seq_h);
|
||||||
|
|
||||||
let (viz_area, sequencer_area) = match layout {
|
|
||||||
MainLayout::Top => {
|
|
||||||
let viz_height = if has_viz { 16 } else { 0 };
|
|
||||||
let [viz, seq] = Layout::vertical([
|
|
||||||
Constraint::Length(viz_height),
|
|
||||||
Constraint::Fill(1),
|
|
||||||
])
|
|
||||||
.areas(main_area);
|
|
||||||
(viz, seq)
|
|
||||||
}
|
|
||||||
MainLayout::Bottom => {
|
|
||||||
let viz_height = if has_viz { 16 } else { 0 };
|
|
||||||
let [seq, viz] = Layout::vertical([
|
|
||||||
Constraint::Fill(1),
|
|
||||||
Constraint::Length(viz_height),
|
|
||||||
])
|
|
||||||
.areas(main_area);
|
|
||||||
(viz, seq)
|
|
||||||
}
|
|
||||||
MainLayout::Left => {
|
|
||||||
let viz_width = if has_viz { 33 } else { 0 };
|
|
||||||
let [viz, _spacer, seq] = Layout::horizontal([
|
|
||||||
Constraint::Percentage(viz_width),
|
|
||||||
Constraint::Length(2),
|
|
||||||
Constraint::Fill(1),
|
|
||||||
])
|
|
||||||
.areas(main_area);
|
|
||||||
(viz, seq)
|
|
||||||
}
|
|
||||||
MainLayout::Right => {
|
|
||||||
let viz_width = if has_viz { 33 } else { 0 };
|
|
||||||
let [seq, _spacer, viz] = Layout::horizontal([
|
|
||||||
Constraint::Fill(1),
|
|
||||||
Constraint::Length(2),
|
|
||||||
Constraint::Percentage(viz_width),
|
|
||||||
])
|
|
||||||
.areas(main_area);
|
|
||||||
(viz, seq)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if has_viz {
|
if has_viz {
|
||||||
render_viz_area(frame, app, snapshot, viz_area);
|
render_viz_area(frame, app, snapshot, viz_area);
|
||||||
@@ -82,7 +39,6 @@ pub fn render(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area:
|
|||||||
|
|
||||||
render_sequencer(frame, app, snapshot, sequencer_area);
|
render_sequencer(frame, app, snapshot, sequencer_area);
|
||||||
render_vu_meter(frame, app, vu_area);
|
render_vu_meter(frame, app, vu_area);
|
||||||
render_active_patterns(frame, app, snapshot, patterns_area);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum VizPanel {
|
enum VizPanel {
|
||||||
@@ -131,6 +87,131 @@ fn render_viz_area(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const STEPS_PER_PAGE: usize = 32;
|
const STEPS_PER_PAGE: usize = 32;
|
||||||
|
const TILE_HEIGHT: u16 = 3;
|
||||||
|
const ROW_GAP: u16 = 1;
|
||||||
|
|
||||||
|
pub fn sequencer_height(pattern_length: usize, current_step: usize) -> u16 {
|
||||||
|
let page = current_step / STEPS_PER_PAGE;
|
||||||
|
let page_start = page * STEPS_PER_PAGE;
|
||||||
|
let steps_on_page = (page_start + STEPS_PER_PAGE).min(pattern_length) - page_start;
|
||||||
|
if steps_on_page == 0 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let num_rows = steps_on_page.div_ceil(8);
|
||||||
|
let grid_h = (num_rows as u16) * TILE_HEIGHT + (num_rows.saturating_sub(1) as u16) * ROW_GAP;
|
||||||
|
grid_h + 2
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn viz_seq_split(
|
||||||
|
main_area: Rect,
|
||||||
|
layout: MainLayout,
|
||||||
|
has_viz: bool,
|
||||||
|
seq_h: u16,
|
||||||
|
) -> (Rect, Rect) {
|
||||||
|
match layout {
|
||||||
|
MainLayout::Top => {
|
||||||
|
if has_viz {
|
||||||
|
let [viz, seq] = Layout::vertical([
|
||||||
|
Constraint::Fill(1),
|
||||||
|
Constraint::Length(seq_h),
|
||||||
|
])
|
||||||
|
.areas(main_area);
|
||||||
|
(viz, seq)
|
||||||
|
} else {
|
||||||
|
(Rect::default(), main_area)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MainLayout::Bottom => {
|
||||||
|
if has_viz {
|
||||||
|
let [seq, viz] = Layout::vertical([
|
||||||
|
Constraint::Length(seq_h),
|
||||||
|
Constraint::Fill(1),
|
||||||
|
])
|
||||||
|
.areas(main_area);
|
||||||
|
(viz, seq)
|
||||||
|
} else {
|
||||||
|
(Rect::default(), main_area)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MainLayout::Left => {
|
||||||
|
let viz_width = if has_viz { 33 } else { 0 };
|
||||||
|
let [viz, _spacer, seq] = Layout::horizontal([
|
||||||
|
Constraint::Percentage(viz_width),
|
||||||
|
Constraint::Length(2),
|
||||||
|
Constraint::Fill(1),
|
||||||
|
])
|
||||||
|
.areas(main_area);
|
||||||
|
(viz, seq)
|
||||||
|
}
|
||||||
|
MainLayout::Right => {
|
||||||
|
let viz_width = if has_viz { 33 } else { 0 };
|
||||||
|
let [seq, _spacer, viz] = Layout::horizontal([
|
||||||
|
Constraint::Fill(1),
|
||||||
|
Constraint::Length(2),
|
||||||
|
Constraint::Percentage(viz_width),
|
||||||
|
])
|
||||||
|
.areas(main_area);
|
||||||
|
(viz, seq)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn grid_layout(area: Rect, steps_on_page: usize) -> Vec<(Rect, usize)> {
|
||||||
|
if area.width < 50 || steps_on_page == 0 {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
let num_rows = steps_on_page.div_ceil(8);
|
||||||
|
let steps_per_row = steps_on_page.div_ceil(num_rows);
|
||||||
|
|
||||||
|
let total_grid_height =
|
||||||
|
(num_rows as u16) * TILE_HEIGHT + (num_rows.saturating_sub(1) as u16) * ROW_GAP;
|
||||||
|
let y_offset = area.height.saturating_sub(total_grid_height) / 2;
|
||||||
|
|
||||||
|
let grid_area = Rect {
|
||||||
|
x: area.x,
|
||||||
|
y: area.y + y_offset,
|
||||||
|
width: area.width,
|
||||||
|
height: total_grid_height.min(area.height),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut row_constraints: Vec<Constraint> = Vec::new();
|
||||||
|
for i in 0..num_rows {
|
||||||
|
row_constraints.push(Constraint::Length(TILE_HEIGHT));
|
||||||
|
if i < num_rows - 1 {
|
||||||
|
row_constraints.push(Constraint::Length(ROW_GAP));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let row_areas = Layout::vertical(row_constraints).split(grid_area);
|
||||||
|
|
||||||
|
let mut tiles = Vec::with_capacity(steps_on_page);
|
||||||
|
|
||||||
|
for row_idx in 0..num_rows {
|
||||||
|
let row_area = row_areas[row_idx * 2];
|
||||||
|
let start_step = row_idx * steps_per_row;
|
||||||
|
let end_step = (start_step + steps_per_row).min(steps_on_page);
|
||||||
|
let cols_in_row = end_step - start_step;
|
||||||
|
|
||||||
|
let mut col_constraints: Vec<Constraint> = Vec::new();
|
||||||
|
for col in 0..cols_in_row {
|
||||||
|
col_constraints.push(Constraint::Fill(1));
|
||||||
|
if col < cols_in_row - 1 {
|
||||||
|
if (col + 1) % 4 == 0 {
|
||||||
|
col_constraints.push(Constraint::Length(2));
|
||||||
|
} else {
|
||||||
|
col_constraints.push(Constraint::Length(1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let col_areas = Layout::horizontal(col_constraints).split(row_area);
|
||||||
|
|
||||||
|
for col_idx in 0..cols_in_row {
|
||||||
|
tiles.push((col_areas[col_idx * 2], start_step + col_idx));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tiles
|
||||||
|
}
|
||||||
|
|
||||||
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();
|
let theme = theme::get();
|
||||||
@@ -149,43 +230,12 @@ fn render_sequencer(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot,
|
|||||||
let page_start = page * STEPS_PER_PAGE;
|
let page_start = page * STEPS_PER_PAGE;
|
||||||
let steps_on_page = (page_start + STEPS_PER_PAGE).min(length) - page_start;
|
let steps_on_page = (page_start + STEPS_PER_PAGE).min(length) - page_start;
|
||||||
|
|
||||||
let num_rows = steps_on_page.div_ceil(8);
|
for (tile_rect, step_offset) in grid_layout(area, steps_on_page) {
|
||||||
let steps_per_row = steps_on_page.div_ceil(num_rows);
|
let step_idx = page_start + step_offset;
|
||||||
|
|
||||||
let row_height = area.height / num_rows as u16;
|
|
||||||
|
|
||||||
let row_constraints: Vec<Constraint> = (0..num_rows)
|
|
||||||
.map(|_| Constraint::Length(row_height))
|
|
||||||
.collect();
|
|
||||||
let rows = Layout::vertical(row_constraints).split(area);
|
|
||||||
|
|
||||||
for row_idx in 0..num_rows {
|
|
||||||
let row_area = rows[row_idx];
|
|
||||||
let start_step = row_idx * steps_per_row;
|
|
||||||
let end_step = (start_step + steps_per_row).min(steps_on_page);
|
|
||||||
let cols_in_row = end_step - start_step;
|
|
||||||
|
|
||||||
let col_constraints: Vec<Constraint> = (0..cols_in_row * 2 - 1)
|
|
||||||
.map(|i| {
|
|
||||||
if i % 2 == 0 {
|
|
||||||
Constraint::Fill(1)
|
|
||||||
} else if i == cols_in_row - 1 {
|
|
||||||
Constraint::Length(2)
|
|
||||||
} else {
|
|
||||||
Constraint::Length(1)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
let cols = Layout::horizontal(col_constraints).split(row_area);
|
|
||||||
|
|
||||||
for col_idx in 0..cols_in_row {
|
|
||||||
let step_idx = page_start + start_step + col_idx;
|
|
||||||
if step_idx < length {
|
if step_idx < length {
|
||||||
render_tile(frame, cols[col_idx * 2], app, snapshot, step_idx);
|
render_tile(frame, tile_rect, app, snapshot, step_idx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_tile(
|
fn render_tile(
|
||||||
@@ -202,7 +252,9 @@ fn render_tile(
|
|||||||
let has_content = step.map(|s| s.has_content()).unwrap_or(false);
|
let has_content = step.map(|s| s.has_content()).unwrap_or(false);
|
||||||
let is_linked = step.map(|s| s.source.is_some()).unwrap_or(false);
|
let is_linked = step.map(|s| s.source.is_some()).unwrap_or(false);
|
||||||
let is_selected = step_idx == app.editor_ctx.step;
|
let is_selected = step_idx == app.editor_ctx.step;
|
||||||
let in_selection = app.editor_ctx.selection_range()
|
let in_selection = app
|
||||||
|
.editor_ctx
|
||||||
|
.selection_range()
|
||||||
.map(|r| r.contains(&step_idx))
|
.map(|r| r.contains(&step_idx))
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
|
|
||||||
@@ -231,7 +283,11 @@ fn render_tile(
|
|||||||
(Color::Rgb(r, g, b), theme.tile.active_fg)
|
(Color::Rgb(r, g, b), theme.tile.active_fg)
|
||||||
}
|
}
|
||||||
(false, true, false, false, _) => {
|
(false, true, false, false, _) => {
|
||||||
let bg = if has_content { theme.tile.content_bg } else { theme.tile.active_bg };
|
let bg = if has_content {
|
||||||
|
theme.tile.content_bg
|
||||||
|
} else {
|
||||||
|
theme.tile.active_bg
|
||||||
|
};
|
||||||
(bg, theme.tile.active_fg)
|
(bg, theme.tile.active_fg)
|
||||||
}
|
}
|
||||||
(false, false, true, _, _) => (theme.selection.selected, theme.selection.cursor_fg),
|
(false, false, true, _, _) => (theme.selection.selected, theme.selection.cursor_fg),
|
||||||
@@ -239,11 +295,8 @@ fn render_tile(
|
|||||||
(false, false, false, _, _) => (theme.tile.inactive_bg, theme.tile.inactive_fg),
|
(false, false, false, _, _) => (theme.tile.inactive_bg, theme.tile.inactive_fg),
|
||||||
};
|
};
|
||||||
|
|
||||||
let block = Block::default()
|
let bg_fill = Paragraph::new("").style(Style::new().bg(bg));
|
||||||
.borders(Borders::ALL)
|
frame.render_widget(bg_fill, area);
|
||||||
.border_style(Style::new().fg(theme.ui.border));
|
|
||||||
let inner = block.inner(area);
|
|
||||||
frame.render_widget(block, area);
|
|
||||||
|
|
||||||
let source_idx = step.and_then(|s| s.source);
|
let source_idx = step.and_then(|s| s.source);
|
||||||
let symbol = if is_playing {
|
let symbol = if is_playing {
|
||||||
@@ -256,53 +309,54 @@ fn render_tile(
|
|||||||
format!("{:02}", step_idx + 1)
|
format!("{:02}", step_idx + 1)
|
||||||
};
|
};
|
||||||
|
|
||||||
// For linked steps, get the name from the source step
|
|
||||||
let step_name = if let Some(src) = source_idx {
|
let step_name = if let Some(src) = source_idx {
|
||||||
pattern.step(src as usize).and_then(|s| s.name.as_ref())
|
pattern.step(src as usize).and_then(|s| s.name.as_ref())
|
||||||
} else {
|
} else {
|
||||||
step.and_then(|s| s.name.as_ref())
|
step.and_then(|s| s.name.as_ref())
|
||||||
};
|
};
|
||||||
let num_lines = if step_name.is_some() { 2u16 } else { 1u16 };
|
|
||||||
let content_height = num_lines;
|
|
||||||
let y_offset = inner.height.saturating_sub(content_height) / 2;
|
|
||||||
|
|
||||||
// Fill background for inner area
|
let center_y = area.y + area.height / 2;
|
||||||
let bg_fill = Paragraph::new("").style(Style::new().bg(bg));
|
|
||||||
frame.render_widget(bg_fill, inner);
|
|
||||||
|
|
||||||
if let Some(name) = step_name {
|
if let Some(name) = step_name {
|
||||||
|
if center_y > area.y {
|
||||||
let name_area = Rect {
|
let name_area = Rect {
|
||||||
x: inner.x,
|
x: area.x,
|
||||||
y: inner.y + y_offset,
|
y: center_y - 1,
|
||||||
width: inner.width,
|
width: area.width,
|
||||||
height: 1,
|
height: 1,
|
||||||
};
|
};
|
||||||
let name_widget = Paragraph::new(name.as_str())
|
let name_widget = Paragraph::new(name.as_str())
|
||||||
.alignment(Alignment::Center)
|
.alignment(Alignment::Center)
|
||||||
.style(Style::new().bg(bg).fg(fg).add_modifier(Modifier::BOLD));
|
.style(Style::new().bg(bg).fg(fg).add_modifier(Modifier::BOLD));
|
||||||
frame.render_widget(name_widget, name_area);
|
frame.render_widget(name_widget, name_area);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let symbol_area = Rect {
|
let symbol_area = Rect {
|
||||||
x: inner.x,
|
x: area.x,
|
||||||
y: inner.y + y_offset + 1,
|
y: center_y,
|
||||||
width: inner.width,
|
width: area.width,
|
||||||
height: 1,
|
height: 1,
|
||||||
};
|
};
|
||||||
let symbol_widget = Paragraph::new(symbol)
|
let symbol_widget = Paragraph::new(symbol)
|
||||||
.alignment(Alignment::Center)
|
.alignment(Alignment::Center)
|
||||||
.style(Style::new().bg(bg).fg(fg).add_modifier(Modifier::BOLD));
|
.style(Style::new().bg(bg).fg(fg).add_modifier(Modifier::BOLD));
|
||||||
frame.render_widget(symbol_widget, symbol_area);
|
frame.render_widget(symbol_widget, symbol_area);
|
||||||
} else {
|
|
||||||
let centered_area = Rect {
|
if has_content && center_y + 1 < area.y + area.height {
|
||||||
x: inner.x,
|
let script = pattern.resolve_script(step_idx).unwrap_or("");
|
||||||
y: inner.y + y_offset,
|
if let Some(first_token) = script.split_whitespace().next() {
|
||||||
width: inner.width,
|
let hint_area = Rect {
|
||||||
|
x: area.x,
|
||||||
|
y: center_y + 1,
|
||||||
|
width: area.width,
|
||||||
height: 1,
|
height: 1,
|
||||||
};
|
};
|
||||||
let tile = Paragraph::new(symbol)
|
let hint_widget = Paragraph::new(first_token)
|
||||||
.alignment(Alignment::Center)
|
.alignment(Alignment::Center)
|
||||||
.style(Style::new().bg(bg).fg(fg).add_modifier(Modifier::BOLD));
|
.style(Style::new().bg(bg).fg(theme.ui.text_dim));
|
||||||
frame.render_widget(tile, centered_area);
|
frame.render_widget(hint_widget, hint_area);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -428,45 +482,3 @@ fn render_vu_meter(frame: &mut Frame, app: &App, area: Rect) {
|
|||||||
frame.render_widget(vu, inner);
|
frame.render_widget(vu, inner);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_active_patterns(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area: Rect) {
|
|
||||||
use crate::widgets::MuteStatus;
|
|
||||||
|
|
||||||
let theme = theme::get();
|
|
||||||
let block = Block::default()
|
|
||||||
.borders(Borders::ALL)
|
|
||||||
.border_style(Style::new().fg(theme.ui.border));
|
|
||||||
let inner = block.inner(area);
|
|
||||||
frame.render_widget(block, area);
|
|
||||||
|
|
||||||
let patterns: Vec<(usize, usize, usize)> = snapshot
|
|
||||||
.active_patterns
|
|
||||||
.iter()
|
|
||||||
.map(|p| (p.bank, p.pattern, p.iter))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let mute_status: Vec<MuteStatus> = snapshot
|
|
||||||
.active_patterns
|
|
||||||
.iter()
|
|
||||||
.map(|p| {
|
|
||||||
if app.mute.is_soloed(p.bank, p.pattern) {
|
|
||||||
MuteStatus::Soloed
|
|
||||||
} else if app.mute.is_muted(p.bank, p.pattern) {
|
|
||||||
MuteStatus::Muted
|
|
||||||
} else if app.mute.is_effectively_muted(p.bank, p.pattern) {
|
|
||||||
MuteStatus::EffectivelyMuted
|
|
||||||
} else {
|
|
||||||
MuteStatus::Normal
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let step_info = snapshot
|
|
||||||
.get_step(app.editor_ctx.bank, app.editor_ctx.pattern)
|
|
||||||
.map(|step| (step, app.current_edit_pattern().length));
|
|
||||||
|
|
||||||
let mut widget = ActivePatterns::new(&patterns).with_mute_status(&mute_status);
|
|
||||||
if let Some((step, total)) = step_info {
|
|
||||||
widget = widget.with_step(step, total);
|
|
||||||
}
|
|
||||||
frame.render_widget(widget, inner);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -9,4 +9,4 @@ pub mod patterns_view;
|
|||||||
mod render;
|
mod render;
|
||||||
pub mod title_view;
|
pub mod title_view;
|
||||||
|
|
||||||
pub use render::render;
|
pub use render::{horizontal_padding, render};
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use crate::app::App;
|
|||||||
use crate::engine::LinkState;
|
use crate::engine::LinkState;
|
||||||
use crate::midi;
|
use crate::midi;
|
||||||
use crate::state::OptionsFocus;
|
use crate::state::OptionsFocus;
|
||||||
use crate::theme;
|
use crate::theme::{self, ThemeColors};
|
||||||
use crate::widgets::{render_scroll_indicators, IndicatorAlign};
|
use crate::widgets::{render_scroll_indicators, IndicatorAlign};
|
||||||
|
|
||||||
pub fn render(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) {
|
pub fn render(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) {
|
||||||
@@ -90,6 +90,12 @@ pub fn render(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) {
|
|||||||
focus == OptionsFocus::ShowPreview,
|
focus == OptionsFocus::ShowPreview,
|
||||||
&theme,
|
&theme,
|
||||||
),
|
),
|
||||||
|
render_option_line(
|
||||||
|
"Performance mode",
|
||||||
|
if app.ui.performance_mode { "On" } else { "Off" },
|
||||||
|
focus == OptionsFocus::PerformanceMode,
|
||||||
|
&theme,
|
||||||
|
),
|
||||||
];
|
];
|
||||||
if app.plugin_mode {
|
if app.plugin_mode {
|
||||||
let zoom_str = format!("{:.0}%", app.ui.zoom_factor * 100.0);
|
let zoom_str = format!("{:.0}%", app.ui.zoom_factor * 100.0);
|
||||||
@@ -246,6 +252,14 @@ pub fn render(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Insert description below focused option
|
||||||
|
let focus_vec_idx = focus.line_index(app.plugin_mode);
|
||||||
|
if let Some(desc) = option_description(focus) {
|
||||||
|
if focus_vec_idx < lines.len() {
|
||||||
|
lines.insert(focus_vec_idx + 1, render_description_line(desc, &theme));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let total_lines = lines.len();
|
let total_lines = lines.len();
|
||||||
let max_visible = padded.height as usize;
|
let max_visible = padded.height as usize;
|
||||||
|
|
||||||
@@ -318,6 +332,42 @@ fn render_option_line(label: &str, value: &str, focused: bool, theme: &theme::Th
|
|||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn option_description(focus: OptionsFocus) -> Option<&'static str> {
|
||||||
|
match focus {
|
||||||
|
OptionsFocus::ColorScheme => Some("Color scheme for the entire interface"),
|
||||||
|
OptionsFocus::HueRotation => Some("Shift all theme colors by a hue angle"),
|
||||||
|
OptionsFocus::RefreshRate => Some("Lower values reduce CPU usage"),
|
||||||
|
OptionsFocus::RuntimeHighlight => Some("Highlight executed code spans during playback"),
|
||||||
|
OptionsFocus::ShowScope => Some("Oscilloscope on the engine page"),
|
||||||
|
OptionsFocus::ShowSpectrum => Some("Spectrum analyzer on the engine page"),
|
||||||
|
OptionsFocus::ShowCompletion => Some("Word completion popup in the editor"),
|
||||||
|
OptionsFocus::ShowPreview => Some("Step script preview on the sequencer grid"),
|
||||||
|
OptionsFocus::PerformanceMode => Some("Hide header and footer bars"),
|
||||||
|
OptionsFocus::Font => Some("Bitmap font for the plugin window"),
|
||||||
|
OptionsFocus::ZoomFactor => Some("Scale factor for the plugin window"),
|
||||||
|
OptionsFocus::WindowSize => Some("Default size for the plugin window"),
|
||||||
|
OptionsFocus::LinkEnabled => Some("Join an Ableton Link session on the local network"),
|
||||||
|
OptionsFocus::StartStopSync => Some("Sync transport start/stop with other Link peers"),
|
||||||
|
OptionsFocus::Quantum => Some("Number of beats per phase cycle"),
|
||||||
|
OptionsFocus::MidiOutput0 => Some("MIDI output device for channel group 1"),
|
||||||
|
OptionsFocus::MidiOutput1 => Some("MIDI output device for channel group 2"),
|
||||||
|
OptionsFocus::MidiOutput2 => Some("MIDI output device for channel group 3"),
|
||||||
|
OptionsFocus::MidiOutput3 => Some("MIDI output device for channel group 4"),
|
||||||
|
OptionsFocus::MidiInput0 => Some("MIDI input device for channel group 1"),
|
||||||
|
OptionsFocus::MidiInput1 => Some("MIDI input device for channel group 2"),
|
||||||
|
OptionsFocus::MidiInput2 => Some("MIDI input device for channel group 3"),
|
||||||
|
OptionsFocus::MidiInput3 => Some("MIDI input device for channel group 4"),
|
||||||
|
OptionsFocus::ResetOnboarding => Some("Re-enable all dismissed guide popups"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_description_line(desc: &str, theme: &ThemeColors) -> Line<'static> {
|
||||||
|
Line::from(Span::styled(
|
||||||
|
format!(" {desc}"),
|
||||||
|
Style::new().fg(theme.ui.text_dim),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
fn render_readonly_line(label: &str, value: &str, value_style: Style, theme: &theme::ThemeColors) -> Line<'static> {
|
fn render_readonly_line(label: &str, value: &str, value_style: Style, theme: &theme::ThemeColors) -> Line<'static> {
|
||||||
let label_style = Style::new().fg(theme.ui.text_muted);
|
let label_style = Style::new().fg(theme.ui.text_muted);
|
||||||
let label_width = 20;
|
let label_width = 20;
|
||||||
|
|||||||
@@ -62,6 +62,16 @@ pub fn adjust_resolved_for_line(
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn horizontal_padding(width: u16) -> u16 {
|
||||||
|
if width >= 120 {
|
||||||
|
4
|
||||||
|
} else if width >= 80 {
|
||||||
|
2
|
||||||
|
} else {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn render(
|
pub fn render(
|
||||||
frame: &mut Frame,
|
frame: &mut Frame,
|
||||||
app: &App,
|
app: &App,
|
||||||
@@ -90,23 +100,28 @@ pub fn render(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let h_pad = horizontal_padding(term.width);
|
||||||
let padded = Rect {
|
let padded = Rect {
|
||||||
x: term.x + 4,
|
x: term.x + h_pad,
|
||||||
y: term.y + 1,
|
y: term.y + 1,
|
||||||
width: term.width.saturating_sub(8),
|
width: term.width.saturating_sub(h_pad * 2),
|
||||||
height: term.height.saturating_sub(2),
|
height: term.height.saturating_sub(2),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let perf = app.ui.performance_mode;
|
||||||
|
|
||||||
let [header_area, _padding, body_area, _bottom_padding, footer_area] = Layout::vertical([
|
let [header_area, _padding, body_area, _bottom_padding, footer_area] = Layout::vertical([
|
||||||
Constraint::Length(header_height(padded.width)),
|
Constraint::Length(if perf { 0 } else { header_height(padded.width) }),
|
||||||
Constraint::Length(1),
|
Constraint::Length(if perf { 0 } else { 1 }),
|
||||||
Constraint::Fill(1),
|
Constraint::Fill(1),
|
||||||
Constraint::Length(1),
|
Constraint::Length(if perf { 0 } else { 1 }),
|
||||||
Constraint::Length(3),
|
Constraint::Length(if perf { 0 } else { 3 }),
|
||||||
])
|
])
|
||||||
.areas(padded);
|
.areas(padded);
|
||||||
|
|
||||||
|
if !perf {
|
||||||
render_header(frame, app, link, snapshot, header_area);
|
render_header(frame, app, link, snapshot, header_area);
|
||||||
|
}
|
||||||
|
|
||||||
let (page_area, panel_area) = if app.panel.visible && app.panel.side.is_some() {
|
let (page_area, panel_area) = if app.panel.visible && app.panel.side.is_some() {
|
||||||
if body_area.width >= 120 {
|
if body_area.width >= 120 {
|
||||||
@@ -139,7 +154,9 @@ pub fn render(
|
|||||||
render_side_panel(frame, app, side_area);
|
render_side_panel(frame, app, side_area);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !perf {
|
||||||
render_footer(frame, app, footer_area);
|
render_footer(frame, app, footer_area);
|
||||||
|
}
|
||||||
let modal_area = render_modal(frame, app, snapshot, term);
|
let modal_area = render_modal(frame, app, snapshot, term);
|
||||||
|
|
||||||
if app.ui.show_minimap() {
|
if app.ui.show_minimap() {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
pub use cagire_ratatui::{
|
pub use cagire_ratatui::{
|
||||||
hint_line, render_props_form, render_scroll_indicators, render_search_bar,
|
hint_line, render_props_form, render_scroll_indicators, render_search_bar,
|
||||||
render_section_header, ActivePatterns, CategoryItem, CategoryList, ConfirmModal,
|
render_section_header, CategoryItem, CategoryList, ConfirmModal, FileBrowserModal,
|
||||||
FileBrowserModal, IndicatorAlign, ModalFrame, MuteStatus, NavMinimap, NavTile, Orientation,
|
IndicatorAlign, ModalFrame, NavMinimap, NavTile, Orientation, SampleBrowser, Scope, Selection,
|
||||||
SampleBrowser, Scope, Selection, Spectrum, TextInputModal, VuMeter, Waveform,
|
Spectrum, TextInputModal, VuMeter, Waveform,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user