Feat: UI / UX improvements once more (mouse)
This commit is contained in:
@@ -418,6 +418,11 @@ impl App {
|
||||
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::CycleScopeMode => self.audio.config.scope_mode = self.audio.config.scope_mode.toggle(),
|
||||
AppCommand::FlipScopeOrientation => self.audio.config.scope_vertical = !self.audio.config.scope_vertical,
|
||||
AppCommand::ToggleLissajousTrails => self.audio.config.lissajous_trails = !self.audio.config.lissajous_trails,
|
||||
AppCommand::CycleSpectrumMode => self.audio.config.spectrum_mode = self.audio.config.spectrum_mode.cycle(),
|
||||
AppCommand::ToggleSpectrumPeaks => self.audio.config.spectrum_peaks = !self.audio.config.spectrum_peaks,
|
||||
AppCommand::TogglePreview => self.audio.config.show_preview = !self.audio.config.show_preview,
|
||||
AppCommand::SetGainBoost(g) => self.audio.config.gain_boost = g,
|
||||
AppCommand::ToggleNormalizeViz => self.audio.config.normalize_viz = !self.audio.config.normalize_viz,
|
||||
|
||||
@@ -278,6 +278,11 @@ pub enum AppCommand {
|
||||
ToggleScope,
|
||||
ToggleSpectrum,
|
||||
ToggleLissajous,
|
||||
CycleScopeMode,
|
||||
FlipScopeOrientation,
|
||||
ToggleLissajousTrails,
|
||||
CycleSpectrumMode,
|
||||
ToggleSpectrumPeaks,
|
||||
TogglePreview,
|
||||
SetGainBoost(f32),
|
||||
ToggleNormalizeViz,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use crossterm::event::{MouseButton, MouseEvent, MouseEventKind};
|
||||
use ratatui::layout::{Constraint, Layout, Rect};
|
||||
|
||||
@@ -11,6 +13,14 @@ use crate::views::{dict_view, engine_view, help_view, main_view, patterns_view,
|
||||
|
||||
use super::InputContext;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum ClickKind {
|
||||
Single,
|
||||
Double,
|
||||
}
|
||||
|
||||
const DOUBLE_CLICK_MS: u128 = 300;
|
||||
|
||||
pub fn handle_mouse(ctx: &mut InputContext, mouse: MouseEvent, term: Rect) {
|
||||
let kind = mouse.kind;
|
||||
let col = mouse.column;
|
||||
@@ -25,7 +35,18 @@ pub fn handle_mouse(ctx: &mut InputContext, mouse: MouseEvent, term: Rect) {
|
||||
}
|
||||
|
||||
match kind {
|
||||
MouseEventKind::Down(MouseButton::Left) => handle_click(ctx, col, row, term),
|
||||
MouseEventKind::Down(MouseButton::Left) => {
|
||||
let now = Instant::now();
|
||||
let click_kind = match ctx.app.ui.last_click.take() {
|
||||
Some((t, c, r)) if now.duration_since(t).as_millis() < DOUBLE_CLICK_MS
|
||||
&& c == col && r == row => ClickKind::Double,
|
||||
_ => {
|
||||
ctx.app.ui.last_click = Some((now, col, row));
|
||||
ClickKind::Single
|
||||
}
|
||||
};
|
||||
handle_click(ctx, col, row, term, click_kind);
|
||||
}
|
||||
MouseEventKind::Drag(MouseButton::Left) | MouseEventKind::Moved => {
|
||||
handle_editor_drag(ctx, col, row, term);
|
||||
handle_script_editor_drag(ctx, col, row, term);
|
||||
@@ -116,7 +137,7 @@ fn handle_editor_mouse(ctx: &mut InputContext, col: u16, row: u16, term: Rect, d
|
||||
.move_cursor_to(text_row, text_col);
|
||||
}
|
||||
|
||||
fn handle_click(ctx: &mut InputContext, col: u16, row: u16, term: Rect) {
|
||||
fn handle_click(ctx: &mut InputContext, col: u16, row: u16, term: Rect, kind: ClickKind) {
|
||||
// Sticky minimap intercepts all clicks
|
||||
if matches!(ctx.app.ui.minimap, MinimapMode::Sticky) {
|
||||
if let Some((gc, gr)) = cagire_ratatui::hit_test_tile(col, row, term) {
|
||||
@@ -144,7 +165,7 @@ fn handle_click(ctx: &mut InputContext, col: u16, row: u16, term: Rect) {
|
||||
} else if contains(footer, col, row) {
|
||||
handle_footer_click(ctx, col, row, footer);
|
||||
} else if contains(body, col, row) {
|
||||
handle_body_click(ctx, col, row, body);
|
||||
handle_body_click(ctx, col, row, body, kind);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,19 +300,30 @@ fn handle_scroll(ctx: &mut InputContext, col: u16, row: u16, term: Rect, up: boo
|
||||
|
||||
// --- Header ---
|
||||
|
||||
fn handle_header_click(ctx: &mut InputContext, col: u16, _row: u16, header: Rect) {
|
||||
let [transport_area, _live, _tempo, _bank, _pattern, _stats] = Layout::horizontal([
|
||||
Constraint::Min(12),
|
||||
Constraint::Length(9),
|
||||
Constraint::Min(14),
|
||||
Constraint::Fill(1),
|
||||
Constraint::Fill(2),
|
||||
Constraint::Min(20),
|
||||
])
|
||||
.areas(header);
|
||||
fn handle_header_click(ctx: &mut InputContext, col: u16, row: u16, header: Rect) {
|
||||
let [logo_area, transport_area, _live, tempo_area, _bank, pattern_area, stats_area] =
|
||||
Layout::horizontal([
|
||||
Constraint::Length(5),
|
||||
Constraint::Min(12),
|
||||
Constraint::Length(9),
|
||||
Constraint::Min(14),
|
||||
Constraint::Fill(1),
|
||||
Constraint::Fill(2),
|
||||
Constraint::Min(20),
|
||||
])
|
||||
.areas(header);
|
||||
|
||||
if contains(transport_area, col, _row) {
|
||||
if contains(logo_area, col, row) {
|
||||
ctx.app.ui.minimap = MinimapMode::Sticky;
|
||||
} else if contains(transport_area, col, row) {
|
||||
ctx.dispatch(AppCommand::TogglePlaying);
|
||||
} else if contains(tempo_area, col, row) {
|
||||
let tempo = format!("{:.1}", ctx.link.tempo());
|
||||
ctx.dispatch(AppCommand::OpenModal(Modal::SetTempo(tempo)));
|
||||
} else if contains(pattern_area, col, row) {
|
||||
ctx.dispatch(AppCommand::GoToPage(Page::Patterns));
|
||||
} else if contains(stats_area, col, row) {
|
||||
ctx.dispatch(AppCommand::GoToPage(Page::Engine));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -325,7 +357,7 @@ fn handle_footer_click(ctx: &mut InputContext, col: u16, row: u16, footer: Rect)
|
||||
|
||||
// --- Body ---
|
||||
|
||||
fn handle_body_click(ctx: &mut InputContext, col: u16, row: u16, body: Rect) {
|
||||
fn handle_body_click(ctx: &mut InputContext, col: u16, row: u16, body: Rect, kind: ClickKind) {
|
||||
// Account for side panel splitting
|
||||
let page_area = if ctx.app.panel.visible && ctx.app.panel.side.is_some() {
|
||||
if body.width >= 120 {
|
||||
@@ -350,25 +382,31 @@ fn handle_body_click(ctx: &mut InputContext, col: u16, row: u16, body: Rect) {
|
||||
}
|
||||
|
||||
match ctx.app.page {
|
||||
Page::Main => handle_main_click(ctx, col, row, page_area),
|
||||
Page::Patterns => handle_patterns_click(ctx, col, row, page_area),
|
||||
Page::Main => handle_main_click(ctx, col, row, page_area, kind),
|
||||
Page::Patterns => handle_patterns_click(ctx, col, row, page_area, kind),
|
||||
Page::Help => handle_help_click(ctx, col, row, page_area),
|
||||
Page::Dict => handle_dict_click(ctx, col, row, page_area),
|
||||
Page::Options => handle_options_click(ctx, col, row, page_area),
|
||||
Page::Engine => handle_engine_click(ctx, col, row, page_area),
|
||||
Page::Engine => handle_engine_click(ctx, col, row, page_area, kind),
|
||||
Page::Script => handle_script_click(ctx, col, row, page_area),
|
||||
}
|
||||
}
|
||||
|
||||
// --- 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, kind: ClickKind) {
|
||||
let [main_area, _, _vu_area] = main_view::layout(area);
|
||||
|
||||
if !contains(main_area, col, row) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check viz area clicks before sequencer
|
||||
if let Some(cmd) = hit_test_main_viz(ctx, col, row, main_area, kind) {
|
||||
ctx.dispatch(cmd);
|
||||
return;
|
||||
}
|
||||
|
||||
let sequencer_area = main_view::sequencer_rect(ctx.app, main_area);
|
||||
|
||||
if !contains(sequencer_area, col, row) {
|
||||
@@ -377,9 +415,105 @@ fn handle_main_click(ctx: &mut InputContext, col: u16, row: u16, area: Rect) {
|
||||
|
||||
if let Some(step) = hit_test_grid(ctx, col, row, sequencer_area) {
|
||||
ctx.dispatch(AppCommand::GoToStep(step));
|
||||
if kind == ClickKind::Double {
|
||||
ctx.dispatch(AppCommand::OpenModal(Modal::Editor));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn hit_test_main_viz(
|
||||
ctx: &InputContext,
|
||||
col: u16,
|
||||
row: u16,
|
||||
main_area: Rect,
|
||||
kind: ClickKind,
|
||||
) -> Option<AppCommand> {
|
||||
use crate::state::MainLayout;
|
||||
|
||||
let layout = ctx.app.audio.config.layout;
|
||||
let show_scope = ctx.app.audio.config.show_scope;
|
||||
let show_spectrum = ctx.app.audio.config.show_spectrum;
|
||||
let show_lissajous = ctx.app.audio.config.show_lissajous;
|
||||
let show_preview = ctx.app.audio.config.show_preview;
|
||||
|
||||
let has_viz = show_scope || show_spectrum || show_lissajous || show_preview;
|
||||
if !has_viz {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Determine viz area based on layout
|
||||
let viz_area = if matches!(layout, MainLayout::Top) {
|
||||
// Top layout: render_audio_viz uses only audio panels (no preview)
|
||||
let has_audio_viz = show_scope || show_spectrum || show_lissajous;
|
||||
if !has_audio_viz {
|
||||
return None;
|
||||
}
|
||||
let mut constraints = Vec::new();
|
||||
if has_audio_viz {
|
||||
constraints.push(Constraint::Fill(1));
|
||||
}
|
||||
if show_preview {
|
||||
let ph = if has_audio_viz { 10u16 } else { 14 };
|
||||
constraints.push(Constraint::Length(ph));
|
||||
}
|
||||
constraints.push(Constraint::Fill(1));
|
||||
let areas = Layout::vertical(&constraints).split(main_area);
|
||||
areas[0]
|
||||
} else {
|
||||
let (viz, _) = main_view::viz_seq_split(main_area, layout, has_viz);
|
||||
viz
|
||||
};
|
||||
|
||||
if !contains(viz_area, col, row) {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Build panel list matching render order
|
||||
let is_vertical_layout = matches!(layout, MainLayout::Left | MainLayout::Right);
|
||||
let mut panels: Vec<&str> = Vec::new();
|
||||
if show_scope { panels.push("scope"); }
|
||||
if show_spectrum { panels.push("spectrum"); }
|
||||
if show_lissajous { panels.push("lissajous"); }
|
||||
|
||||
// Top layout uses render_audio_viz (horizontal only, no preview)
|
||||
// Other layouts use render_viz_area (includes preview, vertical if Left/Right)
|
||||
if !matches!(layout, MainLayout::Top) && show_preview {
|
||||
panels.push("preview");
|
||||
}
|
||||
|
||||
if panels.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let constraints: Vec<Constraint> = panels.iter().map(|_| Constraint::Fill(1)).collect();
|
||||
let areas: Vec<Rect> = if is_vertical_layout && !matches!(layout, MainLayout::Top) {
|
||||
Layout::vertical(&constraints).split(viz_area).to_vec()
|
||||
} else {
|
||||
Layout::horizontal(&constraints).split(viz_area).to_vec()
|
||||
};
|
||||
|
||||
for (panel, panel_area) in panels.iter().zip(areas.iter()) {
|
||||
if contains(*panel_area, col, row) {
|
||||
return match *panel {
|
||||
"scope" => Some(if kind == ClickKind::Double {
|
||||
AppCommand::FlipScopeOrientation
|
||||
} else {
|
||||
AppCommand::CycleScopeMode
|
||||
}),
|
||||
"lissajous" => Some(AppCommand::ToggleLissajousTrails),
|
||||
"spectrum" => Some(if kind == ClickKind::Double {
|
||||
AppCommand::ToggleSpectrumPeaks
|
||||
} else {
|
||||
AppCommand::CycleSpectrumMode
|
||||
}),
|
||||
_ => None,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn hit_test_grid(ctx: &InputContext, col: u16, row: u16, area: Rect) -> Option<usize> {
|
||||
let pattern = ctx.app.current_edit_pattern();
|
||||
let length = pattern.length;
|
||||
@@ -402,7 +536,7 @@ fn hit_test_grid(ctx: &InputContext, col: u16, row: u16, area: Rect) -> Option<u
|
||||
|
||||
// --- Patterns page ---
|
||||
|
||||
fn handle_patterns_click(ctx: &mut InputContext, col: u16, row: u16, area: Rect) {
|
||||
fn handle_patterns_click(ctx: &mut InputContext, col: u16, row: u16, area: Rect, kind: ClickKind) {
|
||||
let [banks_area, patterns_area, _] = patterns_view::layout(area);
|
||||
|
||||
if contains(banks_area, col, row) {
|
||||
@@ -414,6 +548,9 @@ fn handle_patterns_click(ctx: &mut InputContext, col: u16, row: u16, area: Rect)
|
||||
if let Some(pattern) = hit_test_patterns_list(ctx, col, row, patterns_area, false) {
|
||||
ctx.app.patterns_nav.column = PatternsColumn::Patterns;
|
||||
ctx.dispatch(AppCommand::PatternsSelectPattern(pattern));
|
||||
if kind == ClickKind::Double {
|
||||
ctx.dispatch(AppCommand::PatternsEnter);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -787,8 +924,37 @@ fn handle_script_editor_mouse(
|
||||
ctx.app.script_editor.editor.move_cursor_to(text_row, text_col);
|
||||
}
|
||||
|
||||
fn handle_engine_click(ctx: &mut InputContext, col: u16, row: u16, area: Rect) {
|
||||
let [left_col, _, _] = engine_view::layout(area);
|
||||
fn handle_engine_click(ctx: &mut InputContext, col: u16, row: u16, area: Rect, kind: ClickKind) {
|
||||
let [left_col, _, right_col] = engine_view::layout(area);
|
||||
|
||||
// Viz panel clicks (right column)
|
||||
if contains(right_col, col, row) {
|
||||
let [scope_area, _, lissajous_area, _, spectrum_area] = Layout::vertical([
|
||||
Constraint::Fill(1),
|
||||
Constraint::Length(1),
|
||||
Constraint::Fill(1),
|
||||
Constraint::Length(1),
|
||||
Constraint::Fill(1),
|
||||
])
|
||||
.areas(right_col);
|
||||
|
||||
if contains(scope_area, col, row) {
|
||||
if kind == ClickKind::Double {
|
||||
ctx.dispatch(AppCommand::FlipScopeOrientation);
|
||||
} else {
|
||||
ctx.dispatch(AppCommand::CycleScopeMode);
|
||||
}
|
||||
} else if contains(lissajous_area, col, row) {
|
||||
ctx.dispatch(AppCommand::ToggleLissajousTrails);
|
||||
} else if contains(spectrum_area, col, row) {
|
||||
if kind == ClickKind::Double {
|
||||
ctx.dispatch(AppCommand::ToggleSpectrumPeaks);
|
||||
} else {
|
||||
ctx.dispatch(AppCommand::CycleSpectrumMode);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if !contains(left_col, col, row) {
|
||||
return;
|
||||
|
||||
@@ -17,6 +17,40 @@ impl CyclicEnum for MainLayout {
|
||||
const VARIANTS: &'static [Self] = &[Self::Top, Self::Bottom, Self::Left, Self::Right];
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum ScopeMode {
|
||||
#[default]
|
||||
Line,
|
||||
Filled,
|
||||
}
|
||||
|
||||
impl ScopeMode {
|
||||
pub fn toggle(self) -> Self {
|
||||
match self {
|
||||
Self::Line => Self::Filled,
|
||||
Self::Filled => Self::Line,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum SpectrumMode {
|
||||
#[default]
|
||||
Bars,
|
||||
Line,
|
||||
Filled,
|
||||
}
|
||||
|
||||
impl SpectrumMode {
|
||||
pub fn cycle(self) -> Self {
|
||||
match self {
|
||||
Self::Bars => Self::Line,
|
||||
Self::Line => Self::Filled,
|
||||
Self::Filled => Self::Bars,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum RefreshRate {
|
||||
#[default]
|
||||
@@ -88,6 +122,11 @@ pub struct AudioConfig {
|
||||
pub gain_boost: f32,
|
||||
pub normalize_viz: bool,
|
||||
pub layout: MainLayout,
|
||||
pub scope_mode: ScopeMode,
|
||||
pub scope_vertical: bool,
|
||||
pub lissajous_trails: bool,
|
||||
pub spectrum_mode: SpectrumMode,
|
||||
pub spectrum_peaks: bool,
|
||||
}
|
||||
|
||||
impl Default for AudioConfig {
|
||||
@@ -110,6 +149,11 @@ impl Default for AudioConfig {
|
||||
gain_boost: 1.0,
|
||||
normalize_viz: false,
|
||||
layout: MainLayout::default(),
|
||||
scope_mode: ScopeMode::default(),
|
||||
scope_vertical: false,
|
||||
lissajous_trails: false,
|
||||
spectrum_mode: SpectrumMode::default(),
|
||||
spectrum_peaks: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ pub mod sample_browser;
|
||||
pub mod undo;
|
||||
pub mod ui;
|
||||
|
||||
pub use audio::{AudioSettings, DeviceKind, EngineSection, MainLayout, Metrics, SettingKind};
|
||||
pub use audio::{AudioSettings, DeviceKind, EngineSection, MainLayout, Metrics, ScopeMode, SettingKind, SpectrumMode};
|
||||
pub use color_scheme::ColorScheme;
|
||||
pub use editor::{
|
||||
CopiedStepData, CopiedSteps, EditorContext, EditorTarget, EuclideanField, PatternField,
|
||||
|
||||
@@ -85,6 +85,7 @@ pub struct UiState {
|
||||
pub demo_index: usize,
|
||||
pub nav_indicator_until: Option<Instant>,
|
||||
pub nav_fx: RefCell<Option<Effect>>,
|
||||
pub last_click: Option<(Instant, u16, u16)>,
|
||||
}
|
||||
|
||||
impl Default for UiState {
|
||||
@@ -139,6 +140,7 @@ impl Default for UiState {
|
||||
demo_index: 0,
|
||||
nav_indicator_until: None,
|
||||
nav_fx: RefCell::new(None),
|
||||
last_click: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,9 +8,10 @@ use ratatui::Frame;
|
||||
use crate::app::App;
|
||||
use crate::state::{DeviceKind, EngineSection, SettingKind};
|
||||
use crate::theme;
|
||||
use crate::state::{ScopeMode, SpectrumMode};
|
||||
use crate::widgets::{
|
||||
render_scroll_indicators, render_section_header, IndicatorAlign, Lissajous, Orientation, Scope,
|
||||
Spectrum,
|
||||
Spectrum, SpectrumStyle, Waveform,
|
||||
};
|
||||
|
||||
pub fn layout(area: Rect) -> [Rect; 3] {
|
||||
@@ -182,12 +183,28 @@ fn render_scope(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let inner = block.inner(area);
|
||||
frame.render_widget(block, area);
|
||||
|
||||
let orientation = if app.audio.config.scope_vertical {
|
||||
Orientation::Vertical
|
||||
} else {
|
||||
Orientation::Horizontal
|
||||
};
|
||||
let gain = viz_gain(&app.metrics.scope, &app.audio.config);
|
||||
let scope = Scope::new(&app.metrics.scope)
|
||||
.orientation(Orientation::Horizontal)
|
||||
.color(theme.meter.low)
|
||||
.gain(gain);
|
||||
frame.render_widget(scope, inner);
|
||||
match app.audio.config.scope_mode {
|
||||
ScopeMode::Line => {
|
||||
let scope = Scope::new(&app.metrics.scope)
|
||||
.orientation(orientation)
|
||||
.color(theme.meter.low)
|
||||
.gain(gain);
|
||||
frame.render_widget(scope, inner);
|
||||
}
|
||||
ScopeMode::Filled => {
|
||||
let waveform = Waveform::new(&app.metrics.scope)
|
||||
.orientation(orientation)
|
||||
.color(theme.meter.low)
|
||||
.gain(gain);
|
||||
frame.render_widget(waveform, inner);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_lissajous(frame: &mut Frame, app: &App, area: Rect) {
|
||||
@@ -209,7 +226,8 @@ fn render_lissajous(frame: &mut Frame, app: &App, area: Rect) {
|
||||
};
|
||||
let lissajous = Lissajous::new(&app.metrics.scope, &app.metrics.scope_right)
|
||||
.color(theme.meter.low)
|
||||
.gain(gain);
|
||||
.gain(gain)
|
||||
.trails(app.audio.config.lissajous_trails);
|
||||
frame.render_widget(lissajous, inner);
|
||||
}
|
||||
|
||||
@@ -228,8 +246,15 @@ fn render_spectrum(frame: &mut Frame, app: &App, area: Rect) {
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
let style = match app.audio.config.spectrum_mode {
|
||||
SpectrumMode::Bars => SpectrumStyle::Bars,
|
||||
SpectrumMode::Line => SpectrumStyle::Line,
|
||||
SpectrumMode::Filled => SpectrumStyle::Filled,
|
||||
};
|
||||
let spectrum = Spectrum::new(&app.metrics.spectrum)
|
||||
.gain(gain);
|
||||
.gain(gain)
|
||||
.style(style)
|
||||
.peaks(app.audio.config.spectrum_peaks);
|
||||
frame.render_widget(spectrum, inner);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,8 @@ use crate::engine::SequencerSnapshot;
|
||||
use crate::state::MainLayout;
|
||||
use crate::theme;
|
||||
use crate::views::render::highlight_script_lines;
|
||||
use crate::widgets::{Lissajous, Orientation, Scope, Spectrum, VuMeter};
|
||||
use crate::state::{ScopeMode, SpectrumMode};
|
||||
use crate::widgets::{Lissajous, Orientation, Scope, Spectrum, SpectrumStyle, VuMeter, Waveform};
|
||||
|
||||
pub fn layout(area: Rect) -> [Rect; 3] {
|
||||
Layout::horizontal([
|
||||
@@ -499,12 +500,28 @@ pub(crate) fn render_scope(frame: &mut Frame, app: &App, area: Rect, orientation
|
||||
let inner = block.inner(area);
|
||||
frame.render_widget(block, area);
|
||||
|
||||
let orientation = if app.audio.config.scope_vertical {
|
||||
Orientation::Vertical
|
||||
} else {
|
||||
orientation
|
||||
};
|
||||
let gain = viz_gain(&app.metrics.scope, &app.audio.config);
|
||||
let scope = Scope::new(&app.metrics.scope)
|
||||
.orientation(orientation)
|
||||
.color(theme.meter.low)
|
||||
.gain(gain);
|
||||
frame.render_widget(scope, inner);
|
||||
match app.audio.config.scope_mode {
|
||||
ScopeMode::Line => {
|
||||
let scope = Scope::new(&app.metrics.scope)
|
||||
.orientation(orientation)
|
||||
.color(theme.meter.low)
|
||||
.gain(gain);
|
||||
frame.render_widget(scope, inner);
|
||||
}
|
||||
ScopeMode::Filled => {
|
||||
let waveform = Waveform::new(&app.metrics.scope)
|
||||
.orientation(orientation)
|
||||
.color(theme.meter.low)
|
||||
.gain(gain);
|
||||
frame.render_widget(waveform, inner);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn render_spectrum(frame: &mut Frame, app: &App, area: Rect) {
|
||||
@@ -520,8 +537,15 @@ pub(crate) fn render_spectrum(frame: &mut Frame, app: &App, area: Rect) {
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
let style = match app.audio.config.spectrum_mode {
|
||||
SpectrumMode::Bars => SpectrumStyle::Bars,
|
||||
SpectrumMode::Line => SpectrumStyle::Line,
|
||||
SpectrumMode::Filled => SpectrumStyle::Filled,
|
||||
};
|
||||
let spectrum = Spectrum::new(&app.metrics.spectrum)
|
||||
.gain(gain);
|
||||
.gain(gain)
|
||||
.style(style)
|
||||
.peaks(app.audio.config.spectrum_peaks);
|
||||
frame.render_widget(spectrum, inner);
|
||||
}
|
||||
|
||||
@@ -542,7 +566,8 @@ pub(crate) fn render_lissajous(frame: &mut Frame, app: &App, area: Rect) {
|
||||
};
|
||||
let lissajous = Lissajous::new(&app.metrics.scope, &app.metrics.scope_right)
|
||||
.color(theme.meter.low)
|
||||
.gain(gain);
|
||||
.gain(gain)
|
||||
.trails(app.audio.config.lissajous_trails);
|
||||
frame.render_widget(lissajous, inner);
|
||||
}
|
||||
|
||||
|
||||
@@ -371,8 +371,9 @@ fn render_header(
|
||||
|
||||
let pad = Padding::vertical(1);
|
||||
|
||||
let [transport_area, live_area, tempo_area, bank_area, pattern_area, stats_area] =
|
||||
let [logo_area, transport_area, live_area, tempo_area, bank_area, pattern_area, stats_area] =
|
||||
Layout::horizontal([
|
||||
Constraint::Length(5),
|
||||
Constraint::Min(12),
|
||||
Constraint::Length(9),
|
||||
Constraint::Min(14),
|
||||
@@ -382,6 +383,18 @@ fn render_header(
|
||||
])
|
||||
.areas(area);
|
||||
|
||||
// Logo
|
||||
let logo_style = Style::new()
|
||||
.bg(theme.header.bank_bg)
|
||||
.fg(theme.ui.accent)
|
||||
.add_modifier(Modifier::BOLD);
|
||||
frame.render_widget(
|
||||
Paragraph::new("\u{28ff}")
|
||||
.block(Block::default().padding(pad).style(logo_style))
|
||||
.alignment(Alignment::Center),
|
||||
logo_area,
|
||||
);
|
||||
|
||||
// Transport block
|
||||
let (transport_bg, transport_text) = if app.playback.playing {
|
||||
(theme.status.playing_bg, " ▶ PLAYING ")
|
||||
|
||||
@@ -2,5 +2,5 @@ pub use cagire_ratatui::{
|
||||
hint_line, render_props_form, render_scroll_indicators, render_search_bar,
|
||||
render_section_header, CategoryItem, CategoryList, ConfirmModal, FileBrowserModal,
|
||||
IndicatorAlign, Lissajous, ModalFrame, NavMinimap, NavTile, Orientation, SampleBrowser, Scope,
|
||||
Selection, Spectrum, TextInputModal, VuMeter, Waveform,
|
||||
Selection, Spectrum, SpectrumStyle, TextInputModal, VuMeter, Waveform,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user