use super::*;
pub(super) fn render_clip_view_tabs(frame: &mut Frame, area: Rect, nav: &NavState) {
let focused = nav.focused_pane == Pane::ClipView;
let mut spans: Vec<Span> = Vec::new();
let buf3_style = if focused { theme::amber_bright() } else { theme::dim() };
spans.push(Span::styled("\u{00B3}", buf3_style)); spans.push(Span::styled(" ", theme::bg()));
for tab in [FxPanelTab::TrackFx, FxPanelTab::Synth] {
let active = nav.clip_view.fx_panel_tab == tab && nav.clip_view.focus == ClipViewFocus::FxPanel;
let s = if active { theme::amber_bright().add_modifier(Modifier::BOLD) }
else if focused { theme::normal() }
else { theme::dim() };
spans.push(Span::styled(format!("[{}]", tab.label()), s));
spans.push(Span::styled(" ", theme::bg()));
}
spans.push(Span::styled(" \u{2502} ", theme::border_style()));
for tab in ClipTab::ALL {
let active = nav.clip_view.clip_tab == *tab && nav.clip_view.focus == ClipViewFocus::PianoRoll;
let s = if active { theme::amber_bright().add_modifier(Modifier::BOLD) }
else if focused { theme::normal() }
else { theme::dim() };
spans.push(Span::styled(format!("[{}]", tab.label()), s));
spans.push(Span::styled(" ", theme::bg()));
}
if let Some(t) = nav.active_clip_track() {
if let Some(c) = nav.active_clip() {
spans.push(Span::styled(
format!(" {} \u{00B7} clip {}", t.name.to_uppercase(), c.number),
theme::muted()));
}
}
frame.render_widget(Paragraph::new(Line::from(spans)), area);
}
pub(super) fn render_clip_view(frame: &mut Frame, area: Rect, nav: &NavState, snap: &TransportSnapshot) {
let cols = Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Length(FX_PANEL_W), Constraint::Length(1), Constraint::Min(10), ])
.split(area);
render_fx_panel(frame, cols[0], nav);
let sep: Vec<Line> = (0..area.height)
.map(|_| Line::from(Span::styled("\u{2502}", theme::border_style())))
.collect();
frame.render_widget(Paragraph::new(sep), cols[1]);
match nav.clip_view.clip_tab {
ClipTab::InstConfig => render_inst_config(frame, cols[2], nav),
_ => render_piano_roll(frame, cols[2], nav, snap),
}
}
pub(super) fn render_fx_panel(frame: &mut Frame, area: Rect, nav: &NavState) {
let h = area.height as usize;
let w = area.width as usize;
if h == 0 || w == 0 { return; }
let focused = nav.focused_pane == Pane::ClipView && nav.clip_view.focus == ClipViewFocus::FxPanel;
let mut lines: Vec<Line> = Vec::new();
if nav.clip_view.fx_panel_tab == FxPanelTab::Synth {
let track = nav.tracks.get(nav.track_cursor);
let params = track.map(|t| &t.synth_params).cloned().unwrap_or_default();
if params.is_empty() {
lines.push(Line::from(Span::styled(" (no instrument)", theme::dim())));
} else {
let instrument_type = track.and_then(|t| t.instrument_type);
let is_drum = instrument_type == Some(InstrumentType::DrumRack);
let is_dx7 = instrument_type == Some(InstrumentType::DX7);
let is_jupiter = instrument_type == Some(InstrumentType::Jupiter8);
let is_odyssey = instrument_type == Some(InstrumentType::Odyssey);
let is_juno = instrument_type == Some(InstrumentType::Juno60);
let param_names: &[&str] = if is_drum {
&phosphor_dsp::drum_rack::PARAM_NAMES
} else if is_dx7 {
&phosphor_dsp::dx7::PARAM_NAMES
} else if is_jupiter {
&phosphor_dsp::jupiter::PARAM_NAMES
} else if is_odyssey {
&phosphor_dsp::odyssey::PARAM_NAMES
} else if is_juno {
&phosphor_dsp::juno::PARAM_NAMES
} else {
&phosphor_dsp::synth::PARAM_NAMES
};
let param_count = params.len().min(param_names.len());
let visible_rows = h.saturating_sub(2);
let cursor = nav.clip_view.synth_param_cursor;
let scroll_offset = if cursor >= visible_rows {
cursor - visible_rows + 1
} else {
0
};
for (i, &val) in params[..param_count].iter().enumerate().skip(scroll_offset).take(visible_rows) {
let is_cur = focused && nav.clip_view.synth_param_cursor == i;
let name = param_names.get(i).copied().unwrap_or("?");
let indicator = if is_cur { "\u{25B6}" } else { " " };
let name_s = if is_cur { theme::amber_bright().add_modifier(Modifier::BOLD) } else { theme::normal() };
let dim_s = if is_cur { theme::amber() } else { theme::dim() };
let discrete_label = if is_jupiter {
phosphor_dsp::jupiter::discrete_label(i, val)
} else if is_odyssey {
phosphor_dsp::odyssey::discrete_label(i, val)
} else if is_juno {
phosphor_dsp::juno::discrete_label(i, val)
} else if i == 0 {
Some(if is_drum {
match (val * 10.0) as u8 {
0 => "808", 1 => "909", 2 => "707", 3 => "606", 4 => "777",
5 => "tsty-1", 6 => "tsty-2", 7 => "tsty-3", 8 => "tsty-4", _ => "tsty-5",
}
} else if is_dx7 {
let idx = (val * (phosphor_dsp::dx7::PATCH_COUNT as f32 - 0.01)) as usize;
phosphor_dsp::dx7::PATCH_NAMES[idx.min(phosphor_dsp::dx7::PATCH_COUNT - 1)]
} else {
match (val * 4.0) as u8 {
0 => "sine", 1 => "saw", 2 => "square", _ => "tri",
}
})
} else {
None
};
if let Some(label) = discrete_label {
lines.push(Line::from(vec![
Span::styled(format!(" {indicator} "), name_s),
Span::styled(format!("{name:<8}"), name_s),
Span::styled(format!(" {label}"), dim_s),
]));
} else {
let bar_w = (w.saturating_sub(14)).min(10);
let filled = (val * bar_w as f32) as usize;
let bar: String = "\u{2588}".repeat(filled)
+ &"\u{2591}".repeat(bar_w.saturating_sub(filled));
let display_val = match i {
7 | 8 | 10 => format!("{:.0}ms", val * 2000.0), _ => format!("{:.0}%", val * 100.0),
};
lines.push(Line::from(vec![
Span::styled(format!(" {indicator} "), name_s),
Span::styled(format!("{name:<8}"), name_s),
Span::styled(bar, if is_cur { theme::amber() } else { theme::muted() }),
Span::styled(format!(" {display_val}"), dim_s),
]));
}
}
if focused {
lines.push(Line::from(""));
lines.push(Line::from(vec![
Span::styled(" h/l", theme::dim()),
Span::styled(" adjust ", theme::muted()),
Span::styled("jk", theme::dim()),
Span::styled(" select", theme::muted()),
]));
}
}
} else {
let fx_chain: &[FxInstance] = nav.active_clip_track()
.map(|t| t.fx_chain.as_slice())
.unwrap_or(&[]);
if fx_chain.is_empty() {
lines.push(Line::from(Span::styled(" (no fx)", theme::dim())));
lines.push(Line::from(Span::styled(" enter on [fx]", theme::dim())));
lines.push(Line::from(Span::styled(" to add", theme::dim())));
} else {
for (i, fx) in fx_chain.iter().enumerate() {
let is_cur = focused && nav.clip_view.fx_cursor == i;
let s = if is_cur { theme::amber_bright().add_modifier(Modifier::BOLD) }
else if fx.enabled { theme::normal() }
else { theme::dim() };
let indicator = if is_cur { "\u{25B6}" } else { " " };
let enabled = if fx.enabled { "\u{25CF}" } else { "\u{25CB}" };
lines.push(Line::from(vec![
Span::styled(format!(" {indicator} {enabled} "), s),
Span::styled(fx.fx_type.label(), s),
]));
if is_cur {
for (name, val) in &fx.params {
let bar_w = 8;
let filled = ((val * bar_w as f32) as usize).min(bar_w);
let bar: String = "\u{2588}".repeat(filled) + &"\u{2591}".repeat(bar_w - filled);
lines.push(Line::from(vec![
Span::styled(format!(" {name:<6}"), theme::dim()),
Span::styled(bar, theme::muted()),
Span::styled(format!(" {:.0}%", val * 100.0), theme::dim()),
]));
}
}
}
}
}
lines.truncate(h);
frame.render_widget(Paragraph::new(lines), area);
}
pub(super) fn render_inst_config(frame: &mut Frame, area: Rect, nav: &NavState) {
let (w, h) = (area.width as usize, area.height as usize);
if w == 0 || h == 0 { return; }
let focused = nav.focused_pane == Pane::ClipView
&& nav.clip_view.focus == ClipViewFocus::PianoRoll
&& nav.clip_view.clip_tab == ClipTab::InstConfig;
let track = match nav.active_clip_track().or_else(|| nav.current_track()) {
Some(t) => t,
None => {
frame.render_widget(Paragraph::new(Span::styled(" select a track", theme::dim())), area);
return;
}
};
let inst_label = track.instrument_type.map(|i| i.label()).unwrap_or("—");
let mut lines: Vec<Line> = Vec::new();
lines.push(Line::from(vec![
Span::styled(format!(" {inst_label}"), theme::amber_bright().add_modifier(Modifier::BOLD)),
Span::styled(" instrument config", theme::dim()),
]));
lines.push(Line::from(""));
let sections = [
("LFO", &["rate", "depth", "wave", "target"][..]),
("Filter", &["type", "cutoff", "reso", "env amt"]),
("Envelope", &["attack", "decay", "sustain", "release"]),
("Pitch", &["bend range", "portamento", "detune"]),
];
let cursor = nav.clip_view.inst_config_cursor;
let mut param_idx = 0;
for (section_name, params) in §ions {
lines.push(Line::from(Span::styled(
format!(" {section_name}"),
theme::normal().add_modifier(Modifier::BOLD),
)));
for ¶m_name in *params {
let is_cur = focused && cursor == param_idx;
let indicator = if is_cur { "\u{25B6}" } else { " " };
let name_s = if is_cur { theme::amber_bright().add_modifier(Modifier::BOLD) } else { theme::normal() };
let dim_s = if is_cur { theme::amber() } else { theme::dim() };
let bar_w = (w.saturating_sub(20)).min(12);
let val = 0.0f32; let filled = ((val * bar_w as f32) as usize).min(bar_w);
let bar: String = "\u{2588}".repeat(filled)
+ &"\u{2591}".repeat(bar_w - filled);
lines.push(Line::from(vec![
Span::styled(format!(" {indicator} "), name_s),
Span::styled(format!("{param_name:<12}"), name_s),
Span::styled(bar, if is_cur { theme::amber() } else { theme::muted() }),
Span::styled(format!(" {:.0}%", val * 100.0), dim_s),
]));
param_idx += 1;
}
lines.push(Line::from(""));
}
if focused {
lines.push(Line::from(vec![
Span::styled(" jk", theme::dim()),
Span::styled(" select ", theme::muted()),
Span::styled("hl", theme::dim()),
Span::styled(" adjust ", theme::muted()),
Span::styled("tab", theme::dim()),
Span::styled(" next panel", theme::muted()),
]));
}
frame.render_widget(Paragraph::new(lines), area);
}
pub(super) fn render_piano_roll(frame: &mut Frame, area: Rect, nav: &NavState, snap: &TransportSnapshot) {
let (w, h) = (area.width as usize, area.height as usize);
if w == 0 || h == 0 { return; }
crate::debug_log::log("PIANO", &format!("w={w} h={h} note_w={}", w.saturating_sub(7)));
let track = match nav.active_clip_track() {
Some(t) => t,
None => {
frame.render_widget(Paragraph::new(Span::styled(" select a track", theme::dim())), area);
return;
}
};
let clip = nav.active_clip();
let notes = clip.map(|c| c.notes.as_slice()).unwrap_or(&[]);
let tc = theme::track_color(track.color_index);
let pr = &nav.clip_view.piano_roll;
let focused = nav.focused_pane == Pane::ClipView && nav.clip_view.focus == ClipViewFocus::PianoRoll;
let in_col_mode = focused; let in_row_mode = focused && pr.focus == PianoRollFocus::Row;
let key_w = 6usize;
let note_w = w.saturating_sub(key_w + 1);
let ppq = phosphor_core::transport::Transport::PPQ;
let total_beats = if let Some(c) = clip {
((c.length_ticks as f64) / ppq as f64).ceil() as usize
} else {
16
}.max(1);
let max_visible = (note_w / 3).max(1).min(total_beats);
let scroll_offset = pr.scroll_x.min(total_beats.saturating_sub(max_visible));
let visible_cols = max_visible.min(total_beats - scroll_offset);
let col_w = if note_w > 0 && visible_cols > 0 { note_w / visible_cols } else { 1 };
let mut lines: Vec<Line> = Vec::new();
if in_col_mode && h > 1 {
let mut hdr_spans: Vec<Span> = Vec::new();
if snap.recording {
hdr_spans.push(Span::styled(" \u{25CF}REC", Style::default().fg(theme::REC_ACTIVE).add_modifier(Modifier::BOLD)));
hdr_spans.push(Span::styled(" ", theme::bg()));
} else {
hdr_spans.push(Span::styled(" ", theme::bg()));
}
hdr_spans.push(Span::styled("\u{2502}", theme::border_style()));
for c in 0..visible_cols {
let abs_col = c + scroll_offset; let col_num = abs_col + 1; let is_sel = abs_col == pr.column;
let s = if is_sel {
theme::amber_bright().add_modifier(Modifier::BOLD)
} else {
theme::dim()
};
hdr_spans.push(Span::styled(format!("{:<w$}", col_num, w = col_w), s));
}
lines.push(Line::from(hdr_spans));
}
let rows_for_notes = if in_col_mode && h > 1 { h - 1 } else { h };
for row in 0..rows_for_notes {
let note_i = pr.view_bottom_note as i16 + (rows_for_notes as i16 - 1 - row as i16);
if !(0..=127).contains(¬e_i) {
lines.push(Line::from(Span::styled(" ".repeat(w), theme::bg())));
continue;
}
let note = note_i as u8;
let is_cur = focused && note == pr.cursor_note;
let black = is_black_key(note);
let row_highlighted = pr.is_row_highlighted(note);
let key_bg = if row_highlighted && is_cur {
theme::selection_cursor_bg()
} else if row_highlighted {
theme::selection_bg()
} else if is_cur {
theme::piano_cursor_bg()
} else if black {
theme::piano_black_bg()
} else {
theme::piano_white_bg()
};
let key_fg = if is_cur { theme::AMBER_BRIGHT } else if note % 12 == 0 { theme::NORMAL } else { theme::DIM };
let mut spans: Vec<Span> = Vec::new();
spans.push(Span::styled(format!("{:>5} ", midi_note_name(note)), Style::default().fg(key_fg).bg(key_bg)));
spans.push(Span::styled("\u{2502}",
if note % 12 == 0 { Style::default().fg(theme::grid_major()).bg(theme::bg_val()) }
else { theme::border_style() }));
let row_bg = if row_highlighted && is_cur {
theme::selection_cursor_bg()
} else if row_highlighted {
theme::selection_bg()
} else if is_cur {
theme::piano_cursor_bg()
} else if black {
theme::piano_black_bg()
} else {
theme::piano_white_bg()
};
let mut gr = vec![(' ', Style::default().fg(theme::dim_color_val()).bg(row_bg)); note_w];
for b in 1..visible_cols {
let x = b * col_w;
let abs_b = b + scroll_offset;
if x < note_w {
gr[x] = (if abs_b%4==0 { '\u{2502}' } else { '\u{2506}' },
Style::default().fg(if abs_b%4==0 { theme::grid_major() } else { theme::grid_minor() }).bg(row_bg));
}
}
if let Some((hl_start, hl_end)) = pr.highlight_range() {
let vis_start = hl_start.saturating_sub(scroll_offset);
let vis_end = (hl_end + 1).saturating_sub(scroll_offset);
let hl_x_start = vis_start * col_w;
let hl_x_end = (vis_end * col_w).min(note_w);
let hl_bg = theme::selection_bg();
for x in hl_x_start..hl_x_end {
let (ch, old_s) = gr[x];
let fg = old_s.fg.unwrap_or(theme::DIM);
gr[x] = (ch, Style::default().fg(fg).bg(hl_bg));
}
}
if in_col_mode && pr.column >= scroll_offset && pr.column < scroll_offset + visible_cols {
let vis_col = pr.column - scroll_offset;
let col_start = vis_col * col_w;
let col_end = (col_start + col_w).min(note_w);
let col_bg = if in_row_mode && is_cur {
theme::col_row_bg()
} else if pr.is_highlighted(pr.column) {
theme::selection_cursor_bg()
} else {
theme::col_highlight_bg()
};
for x in col_start..col_end {
let (ch, old_s) = gr[x];
let fg = old_s.fg.unwrap_or(theme::DIM);
gr[x] = (ch, Style::default().fg(fg).bg(col_bg));
}
}
let note_style = Style::default().fg(tc).bg(
if is_cur { theme::piano_cursor_bg() } else { row_bg }
).add_modifier(Modifier::BOLD);
let scroll_frac = if total_beats > 0 { scroll_offset as f64 / total_beats as f64 } else { 0.0 };
let visible_frac = if total_beats > 0 { visible_cols as f64 / total_beats as f64 } else { 1.0 };
for n in notes {
if n.note == note {
let rel_start = (n.start_frac - scroll_frac) / visible_frac;
let rel_end = (n.start_frac + n.duration_frac - scroll_frac) / visible_frac;
if rel_end <= 0.0 || rel_start >= 1.0 { continue; } let sx = (rel_start.max(0.0) * note_w as f64) as usize;
let ex = (rel_end * note_w as f64) as usize;
let ex = ex.max(sx + 1).min(note_w);
for cell in gr.iter_mut().take(ex).skip(sx) {
*cell = ('\u{2588}', note_style);
}
}
}
if snap.playing {
if let Some(clip) = clip {
if clip.length_ticks > 0 {
let pos = snap.position_ticks;
let clip_start = clip.start_tick;
let clip_end = clip_start + clip.length_ticks;
if pos >= clip_start && pos < clip_end {
let frac = (pos - clip_start) as f64 / clip.length_ticks as f64;
let rel = (frac - scroll_frac) / visible_frac;
if rel >= 0.0 && rel < 1.0 {
let x = (rel * note_w as f64) as usize;
if x < note_w {
let (ch, _) = gr[x];
gr[x] = (ch, Style::default().fg(theme::playhead_fg()).bg(theme::playhead_bg()));
}
}
}
}
}
}
let mut text = String::new();
let mut cur_s = Style::default().fg(theme::DIM).bg(row_bg);
for (ch, s) in gr {
if s == cur_s { text.push(ch); }
else {
if !text.is_empty() { spans.push(Span::styled(std::mem::take(&mut text), cur_s)); }
cur_s = s; text.push(ch);
}
}
if !text.is_empty() { spans.push(Span::styled(text, cur_s)); }
lines.push(Line::from(spans));
}
frame.render_widget(Paragraph::new(lines), area);
}