Feat: lissajous
This commit is contained in:
@@ -5,6 +5,7 @@ mod confirm;
|
||||
mod editor;
|
||||
mod file_browser;
|
||||
mod hint_bar;
|
||||
mod lissajous;
|
||||
mod list_select;
|
||||
mod modal;
|
||||
mod nav_minimap;
|
||||
@@ -26,6 +27,7 @@ pub use confirm::ConfirmModal;
|
||||
pub use editor::{fuzzy_match, CompletionCandidate, Editor};
|
||||
pub use file_browser::FileBrowserModal;
|
||||
pub use hint_bar::hint_line;
|
||||
pub use lissajous::Lissajous;
|
||||
pub use list_select::ListSelect;
|
||||
pub use modal::ModalFrame;
|
||||
pub use nav_minimap::{hit_test_tile, minimap_area, NavMinimap, NavTile};
|
||||
|
||||
103
crates/ratatui/src/lissajous.rs
Normal file
103
crates/ratatui/src/lissajous.rs
Normal file
@@ -0,0 +1,103 @@
|
||||
use crate::theme;
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::style::Color;
|
||||
use ratatui::widgets::Widget;
|
||||
use std::cell::RefCell;
|
||||
|
||||
thread_local! {
|
||||
static PATTERNS: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
|
||||
}
|
||||
|
||||
pub struct Lissajous<'a> {
|
||||
left: &'a [f32],
|
||||
right: &'a [f32],
|
||||
color: Option<Color>,
|
||||
}
|
||||
|
||||
impl<'a> Lissajous<'a> {
|
||||
pub fn new(left: &'a [f32], right: &'a [f32]) -> Self {
|
||||
Self {
|
||||
left,
|
||||
right,
|
||||
color: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn color(mut self, c: Color) -> Self {
|
||||
self.color = Some(c);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for Lissajous<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
if area.width == 0 || area.height == 0 || self.left.is_empty() || self.right.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let color = self.color.unwrap_or_else(|| theme::get().meter.low);
|
||||
let width = area.width as usize;
|
||||
let height = area.height as usize;
|
||||
let fine_width = width * 2;
|
||||
let fine_height = height * 4;
|
||||
let len = self.left.len().min(self.right.len());
|
||||
|
||||
let peak = self
|
||||
.left
|
||||
.iter()
|
||||
.chain(self.right.iter())
|
||||
.map(|s| s.abs())
|
||||
.fold(0.0f32, f32::max);
|
||||
let gain = if peak > 0.001 { 1.0 / peak } else { 1.0 };
|
||||
|
||||
PATTERNS.with(|p| {
|
||||
let mut patterns = p.borrow_mut();
|
||||
let size = width * height;
|
||||
patterns.clear();
|
||||
patterns.resize(size, 0);
|
||||
|
||||
for i in 0..len {
|
||||
let l = (self.left[i] * gain).clamp(-1.0, 1.0);
|
||||
let r = (self.right[i] * gain).clamp(-1.0, 1.0);
|
||||
|
||||
// X = right channel, Y = left channel (inverted so up = positive)
|
||||
let fine_x = ((r + 1.0) * 0.5 * (fine_width - 1) as f32).round() as usize;
|
||||
let fine_y = ((1.0 - l) * 0.5 * (fine_height - 1) as f32).round() as usize;
|
||||
let fine_x = fine_x.min(fine_width - 1);
|
||||
let fine_y = fine_y.min(fine_height - 1);
|
||||
|
||||
let char_x = fine_x / 2;
|
||||
let char_y = fine_y / 4;
|
||||
let dot_x = fine_x % 2;
|
||||
let dot_y = fine_y % 4;
|
||||
|
||||
let bit = match (dot_x, dot_y) {
|
||||
(0, 0) => 0x01,
|
||||
(0, 1) => 0x02,
|
||||
(0, 2) => 0x04,
|
||||
(0, 3) => 0x40,
|
||||
(1, 0) => 0x08,
|
||||
(1, 1) => 0x10,
|
||||
(1, 2) => 0x20,
|
||||
(1, 3) => 0x80,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
patterns[char_y * width + char_x] |= bit;
|
||||
}
|
||||
|
||||
for cy in 0..height {
|
||||
for cx in 0..width {
|
||||
let pattern = patterns[cy * width + cx];
|
||||
if pattern != 0 {
|
||||
let ch = char::from_u32(0x2800 + pattern as u32).unwrap_or(' ');
|
||||
buf[(area.x + cx as u16, area.y + cy as u16)]
|
||||
.set_char(ch)
|
||||
.set_fg(color);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -389,6 +389,7 @@ impl App {
|
||||
AppCommand::ToggleRefreshRate => self.audio.toggle_refresh_rate(),
|
||||
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::ToggleLissajous => self.audio.config.show_lissajous = !self.audio.config.show_lissajous,
|
||||
AppCommand::TogglePreview => self.audio.config.show_preview = !self.audio.config.show_preview,
|
||||
AppCommand::TogglePerformanceMode => self.ui.performance_mode = !self.ui.performance_mode,
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ impl App {
|
||||
runtime_highlight: self.ui.runtime_highlight,
|
||||
show_scope: self.audio.config.show_scope,
|
||||
show_spectrum: self.audio.config.show_spectrum,
|
||||
show_lissajous: self.audio.config.show_lissajous,
|
||||
show_preview: self.audio.config.show_preview,
|
||||
show_completion: self.ui.show_completion,
|
||||
performance_mode: self.ui.performance_mode,
|
||||
|
||||
@@ -307,6 +307,7 @@ impl CagireDesktop {
|
||||
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.scope_right = self.scope_buffer.read_right();
|
||||
(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;
|
||||
|
||||
@@ -253,6 +253,7 @@ pub enum AppCommand {
|
||||
ToggleRefreshRate,
|
||||
ToggleScope,
|
||||
ToggleSpectrum,
|
||||
ToggleLissajous,
|
||||
TogglePreview,
|
||||
TogglePerformanceMode,
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ use std::sync::atomic::AtomicU64;
|
||||
|
||||
pub struct ScopeBuffer {
|
||||
pub samples: [AtomicU32; 256],
|
||||
pub samples_right: [AtomicU32; 256],
|
||||
peak_left: AtomicU32,
|
||||
peak_right: AtomicU32,
|
||||
}
|
||||
@@ -25,6 +26,7 @@ impl ScopeBuffer {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
samples: std::array::from_fn(|_| AtomicU32::new(0)),
|
||||
samples_right: std::array::from_fn(|_| AtomicU32::new(0)),
|
||||
peak_left: AtomicU32::new(0),
|
||||
peak_right: AtomicU32::new(0),
|
||||
}
|
||||
@@ -44,10 +46,14 @@ impl ScopeBuffer {
|
||||
|
||||
// Downsample for scope display
|
||||
let frames = data.len() / 2;
|
||||
for (i, atom) in self.samples.iter().enumerate() {
|
||||
for (i, (left_atom, right_atom)) in
|
||||
self.samples.iter().zip(self.samples_right.iter()).enumerate()
|
||||
{
|
||||
let frame_idx = (i * frames) / self.samples.len();
|
||||
let left = data.get(frame_idx * 2).copied().unwrap_or(0.0);
|
||||
atom.store(left.to_bits(), Ordering::Relaxed);
|
||||
let right = data.get(frame_idx * 2 + 1).copied().unwrap_or(0.0);
|
||||
left_atom.store(left.to_bits(), Ordering::Relaxed);
|
||||
right_atom.store(right.to_bits(), Ordering::Relaxed);
|
||||
}
|
||||
|
||||
self.peak_left.store(peak_l.to_bits(), Ordering::Relaxed);
|
||||
@@ -58,6 +64,10 @@ impl ScopeBuffer {
|
||||
std::array::from_fn(|i| f32::from_bits(self.samples[i].load(Ordering::Relaxed)))
|
||||
}
|
||||
|
||||
pub fn read_right(&self) -> [f32; 256] {
|
||||
std::array::from_fn(|i| f32::from_bits(self.samples_right[i].load(Ordering::Relaxed)))
|
||||
}
|
||||
|
||||
pub fn peaks(&self) -> (f32, f32) {
|
||||
let left = f32::from_bits(self.peak_left.load(Ordering::Relaxed));
|
||||
let right = f32::from_bits(self.peak_right.load(Ordering::Relaxed));
|
||||
|
||||
@@ -109,6 +109,7 @@ pub fn init(args: InitArgs) -> Init {
|
||||
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.audio.config.show_lissajous = settings.display.show_lissajous;
|
||||
app.audio.config.show_preview = settings.display.show_preview;
|
||||
app.ui.show_completion = settings.display.show_completion;
|
||||
app.ui.performance_mode = settings.display.performance_mode;
|
||||
|
||||
@@ -24,6 +24,7 @@ pub(crate) fn cycle_option_value(ctx: &mut InputContext, right: bool) {
|
||||
OptionsFocus::RuntimeHighlight => ctx.dispatch(AppCommand::ToggleRuntimeHighlight),
|
||||
OptionsFocus::ShowScope => ctx.dispatch(AppCommand::ToggleScope),
|
||||
OptionsFocus::ShowSpectrum => ctx.dispatch(AppCommand::ToggleSpectrum),
|
||||
OptionsFocus::ShowLissajous => ctx.dispatch(AppCommand::ToggleLissajous),
|
||||
OptionsFocus::ShowCompletion => ctx.dispatch(AppCommand::ToggleCompletion),
|
||||
OptionsFocus::ShowPreview => ctx.dispatch(AppCommand::TogglePreview),
|
||||
OptionsFocus::PerformanceMode => ctx.dispatch(AppCommand::TogglePerformanceMode),
|
||||
|
||||
@@ -231,6 +231,7 @@ fn main() -> io::Result<()> {
|
||||
app.metrics.cpu_load = metrics.load.get_load();
|
||||
app.metrics.schedule_depth = metrics.schedule_depth.load(Ordering::Relaxed) as usize;
|
||||
app.metrics.scope = scope_buffer.read();
|
||||
app.metrics.scope_right = scope_buffer.read_right();
|
||||
(app.metrics.peak_left, app.metrics.peak_right) = scope_buffer.peaks();
|
||||
app.metrics.spectrum = spectrum_buffer.read();
|
||||
app.metrics.nudge_ms = nudge_us.load(Ordering::Relaxed) as f64 / 1000.0;
|
||||
|
||||
@@ -41,6 +41,8 @@ pub struct DisplaySettings {
|
||||
pub show_scope: bool,
|
||||
pub show_spectrum: bool,
|
||||
#[serde(default = "default_true")]
|
||||
pub show_lissajous: bool,
|
||||
#[serde(default = "default_true")]
|
||||
pub show_preview: bool,
|
||||
#[serde(default = "default_true")]
|
||||
pub show_completion: bool,
|
||||
@@ -100,6 +102,7 @@ impl Default for DisplaySettings {
|
||||
runtime_highlight: false,
|
||||
show_scope: true,
|
||||
show_spectrum: true,
|
||||
show_lissajous: true,
|
||||
show_preview: true,
|
||||
show_completion: true,
|
||||
font: default_font(),
|
||||
|
||||
@@ -83,6 +83,7 @@ pub struct AudioConfig {
|
||||
pub refresh_rate: RefreshRate,
|
||||
pub show_scope: bool,
|
||||
pub show_spectrum: bool,
|
||||
pub show_lissajous: bool,
|
||||
pub show_preview: bool,
|
||||
pub layout: MainLayout,
|
||||
}
|
||||
@@ -102,6 +103,7 @@ impl Default for AudioConfig {
|
||||
refresh_rate: RefreshRate::default(),
|
||||
show_scope: true,
|
||||
show_spectrum: true,
|
||||
show_lissajous: true,
|
||||
show_preview: true,
|
||||
layout: MainLayout::default(),
|
||||
}
|
||||
@@ -191,6 +193,7 @@ pub struct Metrics {
|
||||
pub cpu_load: f32,
|
||||
pub schedule_depth: usize,
|
||||
pub scope: [f32; 256],
|
||||
pub scope_right: [f32; 256],
|
||||
pub peak_left: f32,
|
||||
pub peak_right: f32,
|
||||
pub spectrum: [f32; 32],
|
||||
@@ -206,6 +209,7 @@ impl Default for Metrics {
|
||||
cpu_load: 0.0,
|
||||
schedule_depth: 0,
|
||||
scope: [0.0; 256],
|
||||
scope_right: [0.0; 256],
|
||||
peak_left: 0.0,
|
||||
peak_right: 0.0,
|
||||
spectrum: [0.0; 32],
|
||||
|
||||
@@ -9,6 +9,7 @@ pub enum OptionsFocus {
|
||||
RuntimeHighlight,
|
||||
ShowScope,
|
||||
ShowSpectrum,
|
||||
ShowLissajous,
|
||||
ShowCompletion,
|
||||
ShowPreview,
|
||||
PerformanceMode,
|
||||
@@ -38,6 +39,7 @@ impl CyclicEnum for OptionsFocus {
|
||||
Self::RuntimeHighlight,
|
||||
Self::ShowScope,
|
||||
Self::ShowSpectrum,
|
||||
Self::ShowLissajous,
|
||||
Self::ShowCompletion,
|
||||
Self::ShowPreview,
|
||||
Self::PerformanceMode,
|
||||
@@ -93,30 +95,31 @@ const FULL_LAYOUT: &[(OptionsFocus, usize)] = &[
|
||||
(OptionsFocus::RuntimeHighlight, 5),
|
||||
(OptionsFocus::ShowScope, 6),
|
||||
(OptionsFocus::ShowSpectrum, 7),
|
||||
(OptionsFocus::ShowCompletion, 8),
|
||||
(OptionsFocus::ShowPreview, 9),
|
||||
(OptionsFocus::PerformanceMode, 10),
|
||||
(OptionsFocus::Font, 11),
|
||||
(OptionsFocus::ZoomFactor, 12),
|
||||
(OptionsFocus::WindowSize, 13),
|
||||
// blank=14, ABLETON LINK header=15, divider=16
|
||||
(OptionsFocus::LinkEnabled, 17),
|
||||
(OptionsFocus::StartStopSync, 18),
|
||||
(OptionsFocus::Quantum, 19),
|
||||
// blank=20, SESSION header=21, divider=22, Tempo=23, Beat=24, Phase=25
|
||||
// blank=26, MIDI OUTPUTS header=27, divider=28
|
||||
(OptionsFocus::MidiOutput0, 29),
|
||||
(OptionsFocus::MidiOutput1, 30),
|
||||
(OptionsFocus::MidiOutput2, 31),
|
||||
(OptionsFocus::MidiOutput3, 32),
|
||||
// blank=33, MIDI INPUTS header=34, divider=35
|
||||
(OptionsFocus::MidiInput0, 36),
|
||||
(OptionsFocus::MidiInput1, 37),
|
||||
(OptionsFocus::MidiInput2, 38),
|
||||
(OptionsFocus::MidiInput3, 39),
|
||||
// blank=40, ONBOARDING header=41, divider=42
|
||||
(OptionsFocus::ResetOnboarding, 43),
|
||||
(OptionsFocus::LoadDemoOnStartup, 44),
|
||||
(OptionsFocus::ShowLissajous, 8),
|
||||
(OptionsFocus::ShowCompletion, 9),
|
||||
(OptionsFocus::ShowPreview, 10),
|
||||
(OptionsFocus::PerformanceMode, 11),
|
||||
(OptionsFocus::Font, 12),
|
||||
(OptionsFocus::ZoomFactor, 13),
|
||||
(OptionsFocus::WindowSize, 14),
|
||||
// blank=15, ABLETON LINK header=16, divider=17
|
||||
(OptionsFocus::LinkEnabled, 18),
|
||||
(OptionsFocus::StartStopSync, 19),
|
||||
(OptionsFocus::Quantum, 20),
|
||||
// blank=21, SESSION header=22, divider=23, Tempo=24, Beat=25, Phase=26
|
||||
// blank=27, MIDI OUTPUTS header=28, divider=29
|
||||
(OptionsFocus::MidiOutput0, 30),
|
||||
(OptionsFocus::MidiOutput1, 31),
|
||||
(OptionsFocus::MidiOutput2, 32),
|
||||
(OptionsFocus::MidiOutput3, 33),
|
||||
// blank=34, MIDI INPUTS header=35, divider=36
|
||||
(OptionsFocus::MidiInput0, 37),
|
||||
(OptionsFocus::MidiInput1, 38),
|
||||
(OptionsFocus::MidiInput2, 39),
|
||||
(OptionsFocus::MidiInput3, 40),
|
||||
// blank=41, ONBOARDING header=42, divider=43
|
||||
(OptionsFocus::ResetOnboarding, 44),
|
||||
(OptionsFocus::LoadDemoOnStartup, 45),
|
||||
];
|
||||
|
||||
impl OptionsFocus {
|
||||
@@ -172,13 +175,13 @@ fn visible_layout(plugin_mode: bool) -> Vec<(OptionsFocus, usize)> {
|
||||
// based on which sections are hidden.
|
||||
let mut offset: usize = 0;
|
||||
|
||||
// Font/Zoom/Window lines (11,12,13) hidden when !plugin_mode
|
||||
// Font/Zoom/Window lines (12,13,14) hidden when !plugin_mode
|
||||
if !plugin_mode {
|
||||
offset += 3; // 3 lines for Font, ZoomFactor, WindowSize
|
||||
}
|
||||
|
||||
// Link + Session + MIDI sections hidden when plugin_mode
|
||||
// These span from blank(14) through MidiInput3(39) = 26 lines
|
||||
// These span from blank(15) through MidiInput3(40) = 26 lines
|
||||
if plugin_mode {
|
||||
let link_section_lines = 26;
|
||||
offset += link_section_lines;
|
||||
@@ -189,10 +192,10 @@ fn visible_layout(plugin_mode: bool) -> Vec<(OptionsFocus, usize)> {
|
||||
if !focus.is_visible(plugin_mode) {
|
||||
continue;
|
||||
}
|
||||
// Lines at or below index 10 (PerformanceMode) are never shifted
|
||||
let adjusted = if raw_line <= 10 {
|
||||
// Lines at or below index 11 (PerformanceMode) are never shifted
|
||||
let adjusted = if raw_line <= 11 {
|
||||
raw_line
|
||||
} else if !plugin_mode && raw_line <= 13 {
|
||||
} else if !plugin_mode && raw_line <= 14 {
|
||||
// Font/Zoom/Window — these are hidden, skip
|
||||
continue;
|
||||
} else {
|
||||
|
||||
@@ -9,7 +9,8 @@ use crate::app::App;
|
||||
use crate::state::{DeviceKind, EngineSection, SettingKind};
|
||||
use crate::theme;
|
||||
use crate::widgets::{
|
||||
render_scroll_indicators, render_section_header, IndicatorAlign, Orientation, Scope, Spectrum,
|
||||
render_scroll_indicators, render_section_header, IndicatorAlign, Lissajous, Orientation, Scope,
|
||||
Spectrum,
|
||||
};
|
||||
|
||||
pub fn layout(area: Rect) -> [Rect; 3] {
|
||||
@@ -148,14 +149,17 @@ fn render_settings_section(frame: &mut Frame, app: &App, area: Rect) {
|
||||
}
|
||||
|
||||
fn render_visualizers(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let [scope_area, _, spectrum_area] = Layout::vertical([
|
||||
Constraint::Percentage(50),
|
||||
let [scope_area, _, lissajous_area, _gap, spectrum_area] = Layout::vertical([
|
||||
Constraint::Fill(1),
|
||||
Constraint::Length(1),
|
||||
Constraint::Percentage(50),
|
||||
Constraint::Fill(1),
|
||||
Constraint::Length(1),
|
||||
Constraint::Fill(1),
|
||||
])
|
||||
.areas(area);
|
||||
|
||||
render_scope(frame, app, scope_area);
|
||||
render_lissajous(frame, app, lissajous_area);
|
||||
render_spectrum(frame, app, spectrum_area);
|
||||
}
|
||||
|
||||
@@ -175,6 +179,21 @@ fn render_scope(frame: &mut Frame, app: &App, area: Rect) {
|
||||
frame.render_widget(scope, inner);
|
||||
}
|
||||
|
||||
fn render_lissajous(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let theme = theme::get();
|
||||
let block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.title(" Lissajous ")
|
||||
.border_style(Style::new().fg(theme.engine.border_green));
|
||||
|
||||
let inner = block.inner(area);
|
||||
frame.render_widget(block, area);
|
||||
|
||||
let lissajous = Lissajous::new(&app.metrics.scope, &app.metrics.scope_right)
|
||||
.color(theme.meter.low);
|
||||
frame.render_widget(lissajous, inner);
|
||||
}
|
||||
|
||||
fn render_spectrum(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let theme = theme::get();
|
||||
let block = Block::default()
|
||||
|
||||
@@ -12,7 +12,7 @@ use crate::engine::SequencerSnapshot;
|
||||
use crate::state::MainLayout;
|
||||
use crate::theme;
|
||||
use crate::views::render::highlight_script_lines;
|
||||
use crate::widgets::{Orientation, Scope, Spectrum, VuMeter};
|
||||
use crate::widgets::{Lissajous, Orientation, Scope, Spectrum, VuMeter};
|
||||
|
||||
pub fn layout(area: Rect) -> [Rect; 3] {
|
||||
Layout::horizontal([
|
||||
@@ -31,6 +31,7 @@ pub fn render(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area:
|
||||
} else {
|
||||
let has_viz = app.audio.config.show_scope
|
||||
|| app.audio.config.show_spectrum
|
||||
|| app.audio.config.show_lissajous
|
||||
|| app.audio.config.show_preview;
|
||||
let (viz_area, sequencer_area) =
|
||||
viz_seq_split(main_area, app.audio.config.layout, has_viz);
|
||||
@@ -49,7 +50,9 @@ fn render_top_layout(
|
||||
snapshot: &SequencerSnapshot,
|
||||
main_area: Rect,
|
||||
) {
|
||||
let has_audio_viz = app.audio.config.show_scope || app.audio.config.show_spectrum;
|
||||
let has_audio_viz = app.audio.config.show_scope
|
||||
|| app.audio.config.show_spectrum
|
||||
|| app.audio.config.show_lissajous;
|
||||
let has_preview = app.audio.config.show_preview;
|
||||
|
||||
let mut constraints = Vec::new();
|
||||
@@ -85,16 +88,23 @@ fn render_top_layout(
|
||||
}
|
||||
|
||||
fn render_audio_viz(frame: &mut Frame, app: &App, area: Rect) {
|
||||
match (app.audio.config.show_scope, app.audio.config.show_spectrum) {
|
||||
(true, true) => {
|
||||
let [scope_area, spectrum_area] =
|
||||
Layout::horizontal([Constraint::Fill(1), Constraint::Fill(1)]).areas(area);
|
||||
render_scope(frame, app, scope_area, Orientation::Horizontal);
|
||||
render_spectrum(frame, app, spectrum_area);
|
||||
let mut panels: Vec<VizPanel> = Vec::new();
|
||||
if app.audio.config.show_scope { panels.push(VizPanel::Scope); }
|
||||
if app.audio.config.show_spectrum { panels.push(VizPanel::Spectrum); }
|
||||
if app.audio.config.show_lissajous { panels.push(VizPanel::Lissajous); }
|
||||
|
||||
if panels.is_empty() { return; }
|
||||
|
||||
let constraints: Vec<Constraint> = panels.iter().map(|_| Constraint::Fill(1)).collect();
|
||||
let areas: Vec<Rect> = Layout::horizontal(&constraints).split(area).to_vec();
|
||||
|
||||
for (panel, panel_area) in panels.iter().zip(areas.iter()) {
|
||||
match panel {
|
||||
VizPanel::Scope => render_scope(frame, app, *panel_area, Orientation::Horizontal),
|
||||
VizPanel::Spectrum => render_spectrum(frame, app, *panel_area),
|
||||
VizPanel::Lissajous => render_lissajous(frame, app, *panel_area),
|
||||
VizPanel::Preview => {}
|
||||
}
|
||||
(true, false) => render_scope(frame, app, area, Orientation::Horizontal),
|
||||
(false, true) => render_spectrum(frame, app, area),
|
||||
(false, false) => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,7 +114,9 @@ fn preview_height(has_audio_viz: bool) -> u16 {
|
||||
|
||||
pub fn sequencer_rect(app: &App, main_area: Rect) -> Rect {
|
||||
if matches!(app.audio.config.layout, MainLayout::Top) {
|
||||
let has_audio_viz = app.audio.config.show_scope || app.audio.config.show_spectrum;
|
||||
let has_audio_viz = app.audio.config.show_scope
|
||||
|| app.audio.config.show_spectrum
|
||||
|| app.audio.config.show_lissajous;
|
||||
let has_preview = app.audio.config.show_preview;
|
||||
|
||||
let mut constraints = Vec::new();
|
||||
@@ -121,6 +133,7 @@ pub fn sequencer_rect(app: &App, main_area: Rect) -> Rect {
|
||||
} else {
|
||||
let has_viz = app.audio.config.show_scope
|
||||
|| app.audio.config.show_spectrum
|
||||
|| app.audio.config.show_lissajous
|
||||
|| app.audio.config.show_preview;
|
||||
let (_, seq_area) = viz_seq_split(main_area, app.audio.config.layout, has_viz);
|
||||
seq_area
|
||||
@@ -130,6 +143,7 @@ pub fn sequencer_rect(app: &App, main_area: Rect) -> Rect {
|
||||
enum VizPanel {
|
||||
Scope,
|
||||
Spectrum,
|
||||
Lissajous,
|
||||
Preview,
|
||||
}
|
||||
|
||||
@@ -142,11 +156,13 @@ fn render_viz_area(
|
||||
let is_vertical_layout = matches!(app.audio.config.layout, MainLayout::Left | MainLayout::Right);
|
||||
let show_scope = app.audio.config.show_scope;
|
||||
let show_spectrum = app.audio.config.show_spectrum;
|
||||
let show_lissajous = app.audio.config.show_lissajous;
|
||||
let show_preview = app.audio.config.show_preview;
|
||||
|
||||
let mut panels = Vec::new();
|
||||
if show_scope { panels.push(VizPanel::Scope); }
|
||||
if show_spectrum { panels.push(VizPanel::Spectrum); }
|
||||
if show_lissajous { panels.push(VizPanel::Lissajous); }
|
||||
if show_preview { panels.push(VizPanel::Preview); }
|
||||
|
||||
let constraints: Vec<Constraint> = panels.iter().map(|_| Constraint::Fill(1)).collect();
|
||||
@@ -173,6 +189,7 @@ fn render_viz_area(
|
||||
match panel {
|
||||
VizPanel::Scope => render_scope(frame, app, *panel_area, orientation),
|
||||
VizPanel::Spectrum => render_spectrum(frame, app, *panel_area),
|
||||
VizPanel::Lissajous => render_lissajous(frame, app, *panel_area),
|
||||
VizPanel::Preview => {
|
||||
let user_words = user_words_once.as_ref().unwrap();
|
||||
let has_prelude = !app.project_state.project.prelude.trim().is_empty();
|
||||
@@ -491,6 +508,19 @@ fn render_spectrum(frame: &mut Frame, app: &App, area: Rect) {
|
||||
frame.render_widget(spectrum, inner);
|
||||
}
|
||||
|
||||
fn render_lissajous(frame: &mut Frame, app: &App, area: Rect) {
|
||||
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 lissajous = Lissajous::new(&app.metrics.scope, &app.metrics.scope_right)
|
||||
.color(theme.meter.low);
|
||||
frame.render_widget(lissajous, inner);
|
||||
}
|
||||
|
||||
fn render_script_preview(
|
||||
frame: &mut Frame,
|
||||
app: &App,
|
||||
|
||||
@@ -78,6 +78,16 @@ pub fn render(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) {
|
||||
focus == OptionsFocus::ShowSpectrum,
|
||||
&theme,
|
||||
),
|
||||
render_option_line(
|
||||
"Show lissajous",
|
||||
if app.audio.config.show_lissajous {
|
||||
"On"
|
||||
} else {
|
||||
"Off"
|
||||
},
|
||||
focus == OptionsFocus::ShowLissajous,
|
||||
&theme,
|
||||
),
|
||||
render_option_line(
|
||||
"Completion",
|
||||
if app.ui.show_completion { "On" } else { "Off" },
|
||||
@@ -346,6 +356,7 @@ fn option_description(focus: OptionsFocus) -> Option<&'static str> {
|
||||
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::ShowLissajous => Some("XY stereo phase scope (left vs right)"),
|
||||
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"),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
pub use cagire_ratatui::{
|
||||
hint_line, render_props_form, render_scroll_indicators, render_search_bar,
|
||||
render_section_header, CategoryItem, CategoryList, ConfirmModal, FileBrowserModal,
|
||||
IndicatorAlign, ModalFrame, NavMinimap, NavTile, Orientation, SampleBrowser, Scope, Selection,
|
||||
Spectrum, TextInputModal, VuMeter, Waveform,
|
||||
IndicatorAlign, Lissajous, ModalFrame, NavMinimap, NavTile, Orientation, SampleBrowser, Scope,
|
||||
Selection, Spectrum, TextInputModal, VuMeter, Waveform,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user