Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 83 additions & 17 deletions src/ui/layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,29 +102,63 @@ pub fn compute_dual_layout(total: Rect, vis: &PanelVisibility) -> DualSynthLayou
PanelSlot { expanded_height: WAVEFORM_HEIGHT, is_visible: vis.waveform, growable: false },
];

// Calculate total requested height (before overflow handling)
// Make a mutable copy of visibility so we can auto-collapse on overflow
let mut vis_effective: [bool; 7] = [
panels[0].is_visible, panels[1].is_visible,
panels[2].is_visible, panels[3].is_visible,
panels[4].is_visible, panels[5].is_visible,
panels[6].is_visible,
];

let available = total.height;

// Auto-collapse panels when terminal is too small, lowest priority first.
// Priority order for collapsing (first to collapse → last):
// waveform(6), drum_knobs(5), synth_b_grid(3), synth_b_knobs(2),
// synth_a_grid(1), synth_a_knobs(0), drum_grid(4)
let collapse_order: [usize; 7] = [6, 5, 3, 2, 1, 0, 4];
loop {
let mut used: u16 = fixed;
for (i, p) in panels.iter().enumerate() {
if vis_effective[i] {
used = used.saturating_add(p.expanded_height);
} else {
used = used.saturating_add(COLLAPSED_PANEL_HEIGHT);
}
}
if used <= available {
break;
}
// Find next panel to collapse
if let Some(&idx) = collapse_order.iter().find(|&&i| vis_effective[i]) {
vis_effective[idx] = false;
} else {
break; // everything collapsed, nothing more we can do
}
}

// Calculate total requested height
let mut used: u16 = fixed;
for p in &panels {
if p.is_visible {
for (i, p) in panels.iter().enumerate() {
if vis_effective[i] {
used = used.saturating_add(p.expanded_height);
} else {
used = used.saturating_add(COLLAPSED_PANEL_HEIGHT);
}
}

// Compute extra space to distribute to growable panels
let available = total.height;
let extra = if available > used { available - used } else { 0 };

// Count growable visible panels
let growable_count = panels.iter().filter(|p| p.is_visible && p.growable).count() as u16;
let growable_count = panels.iter().enumerate().filter(|(i, p)| vis_effective[*i] && p.growable).count() as u16;
let extra_per_growable = if growable_count > 0 { extra / growable_count } else { 0 };
let mut extra_remainder = if growable_count > 0 { extra % growable_count } else { 0 };

// Assign heights for each panel
let mut heights: [u16; 7] = [0; 7];
for (i, p) in panels.iter().enumerate() {
if p.is_visible {
if vis_effective[i] {
heights[i] = p.expanded_height;
if p.growable {
heights[i] += extra_per_growable;
Expand All @@ -138,30 +172,36 @@ pub fn compute_dual_layout(total: Rect, vis: &PanelVisibility) -> DualSynthLayou
}
}

// Build rects by walking y offsets
// Build rects by walking y offsets, clamping to terminal bounds
let x = total.x;
let w = total.width;
let y_max = total.y + total.height; // first row outside the buffer
let mut y = total.y;

// Transport
let transport = Rect::new(x, y, w, TRANSPORT_HEIGHT);
y += TRANSPORT_HEIGHT;
let transport_h = TRANSPORT_HEIGHT.min(y_max.saturating_sub(y));
let transport = Rect::new(x, y, w, transport_h);
y += transport_h;

// Helper: allocate a panel rect and advance y
// Helper: allocate a panel rect and advance y, clamping to bounds
let mut panel_rects: [(Rect, Rect); 7] = [(Rect::default(), Rect::default()); 7];
for (i, p) in panels.iter().enumerate() {
let h = heights[i];
for i in 0..panels.len() {
let h = heights[i].min(y_max.saturating_sub(y));
if h == 0 {
continue;
}
let rect = Rect::new(x, y, w, h);
if p.is_visible {
panel_rects[i] = (rect, Rect::default()); // expanded, no collapsed
if vis_effective[i] {
panel_rects[i] = (rect, Rect::default());
} else {
panel_rects[i] = (Rect::default(), rect); // no expanded, collapsed
panel_rects[i] = (Rect::default(), rect);
}
y += h;
}

// Activity bar at the bottom
let activity_bar = Rect::new(x, y, w, ACTIVITY_BAR_HEIGHT);
// Activity bar at the bottom (gets whatever is left, may be 0)
let activity_h = ACTIVITY_BAR_HEIGHT.min(y_max.saturating_sub(y));
let activity_bar = Rect::new(x, y, w, activity_h);

DualSynthLayout {
transport,
Expand Down Expand Up @@ -302,6 +342,32 @@ mod tests {
assert_eq!(sa_knobs_y, after_transport);
}

#[test]
fn small_terminal_no_panic() {
// Terminal too small to fit all panels — must not overflow buffer bounds
let vis = PanelVisibility::default();
let ly = compute_dual_layout(term(22), &vis);

// Every rect must stay within [0, 22)
let all_rects = [
ly.transport,
ly.synth_a_knobs, ly.synth_a_knobs_collapsed,
ly.synth_a_grid, ly.synth_a_grid_collapsed,
ly.synth_b_knobs, ly.synth_b_knobs_collapsed,
ly.synth_b_grid, ly.synth_b_grid_collapsed,
ly.drum_grid, ly.drum_knobs, ly.drum_knobs_collapsed,
ly.waveform, ly.waveform_collapsed,
ly.activity_bar,
];
for r in &all_rects {
assert!(
r.y + r.height <= 22,
"rect {:?} extends past terminal height 22",
r,
);
}
}

#[test]
fn default_visibility_layout() {
// Default: synth B collapsed, everything else expanded
Expand Down
18 changes: 13 additions & 5 deletions src/ui/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,31 +65,37 @@ pub fn render(f: &mut Frame, app: &App) {
synth_knobs::render_synth_knobs(f, ly.synth_a_knobs, app, SynthId::A);
} else {
let focused = matches!(app.ui.focus, FocusSection::SynthAControls);
render_collapsed_bar(f, ly.synth_a_knobs_collapsed, "SYNTH A KNOBS", focused);
let label = format!("SYNTH A KNOBS Pat[{}] Kit[{}]",
app.ui.synth_a.active_pattern + 1, app.ui.synth_a.active_kit + 1);
render_collapsed_bar(f, ly.synth_a_knobs_collapsed, &label, focused);
}

// ── Synth A Grid ─────────────────────────────────────────────
if app.ui.panel_vis.synth_a_grid {
synth_grid::render_synth_grid(f, ly.synth_a_grid, app, SynthId::A);
} else {
let focused = matches!(app.ui.focus, FocusSection::SynthAGrid);
render_collapsed_bar(f, ly.synth_a_grid_collapsed, "SYNTH A GRID", focused);
let label = format!("SYNTH A GRID Pat[{}]", app.ui.synth_a.active_pattern + 1);
render_collapsed_bar(f, ly.synth_a_grid_collapsed, &label, focused);
}

// ── Synth B Knobs ────────────────────────────────────────────
if app.ui.panel_vis.synth_b_knobs {
synth_knobs::render_synth_knobs(f, ly.synth_b_knobs, app, SynthId::B);
} else {
let focused = matches!(app.ui.focus, FocusSection::SynthBControls);
render_collapsed_bar(f, ly.synth_b_knobs_collapsed, "SYNTH B KNOBS", focused);
let label = format!("SYNTH B KNOBS Pat[{}] Kit[{}]",
app.ui.synth_b.active_pattern + 1, app.ui.synth_b.active_kit + 1);
render_collapsed_bar(f, ly.synth_b_knobs_collapsed, &label, focused);
}

// ── Synth B Grid ─────────────────────────────────────────────
if app.ui.panel_vis.synth_b_grid {
synth_grid::render_synth_grid(f, ly.synth_b_grid, app, SynthId::B);
} else {
let focused = matches!(app.ui.focus, FocusSection::SynthBGrid);
render_collapsed_bar(f, ly.synth_b_grid_collapsed, "SYNTH B GRID", focused);
let label = format!("SYNTH B GRID Pat[{}]", app.ui.synth_b.active_pattern + 1);
render_collapsed_bar(f, ly.synth_b_grid_collapsed, &label, focused);
}

// ── Drum Grid ────────────────────────────────────────────────
Expand All @@ -102,7 +108,9 @@ pub fn render(f: &mut Frame, app: &App) {
knobs::render_knobs(f, ly.drum_knobs, app);
} else {
let focused = matches!(app.ui.focus, FocusSection::Knobs);
render_collapsed_bar(f, ly.drum_knobs_collapsed, "DRUM KNOBS", focused);
let label = format!("DRUM KNOBS Pat[{}] Kit[{}]",
app.ui.active_pattern + 1, app.ui.active_kit + 1);
render_collapsed_bar(f, ly.drum_knobs_collapsed, &label, focused);
}

// ── Waveform ─────────────────────────────────────────────────
Expand Down
61 changes: 34 additions & 27 deletions src/ui/transport_bar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,11 @@ fn gauge_spans<'a>(value: f32, width: usize, fill_style: Style, empty_style: Sty
}
}

/// Build a compact status line showing: Label Pat[N] Kit[N] Loop[N]
/// Pattern slot key labels (QWERTYUIOP = patterns 1-10).
const PATTERN_KEYS: [&str; 10] = ["q", "w", "e", "r", "t", "y", "u", "i", "o", "p"];

/// Build a status line with pattern/kit selectors:
/// SA | Pattern: q w e r t y u i o p | Kit: 1 2 3 4 5 6 7 8
fn status_line<'a>(
label: &str,
active_pattern: usize,
Expand All @@ -213,38 +217,41 @@ fn status_line<'a>(
) -> Line<'a> {
let mut spans: Vec<Span<'a>> = Vec::new();

// Section label — highlighted when focused
// Section label
let label_style = if is_focused {
Style::default()
.fg(theme::CYAN)
.add_modifier(Modifier::BOLD)
Style::default().fg(theme::CYAN).add_modifier(Modifier::BOLD)
} else {
Style::default().fg(theme::DIM_TEXT)
};
spans.push(Span::styled(format!("{}: ", label), label_style));
spans.push(Span::styled(format!("{}", label), label_style));

// Pattern indicator (compact)
let pattern_display = if let Some(queued) = queued_pattern {
format!("Pat[{}→{}]", active_pattern + 1, queued + 1)
} else {
format!("Pat[{}]", active_pattern + 1)
};
let pattern_style = if is_focused {
Style::default().fg(theme::AMBER)
} else {
Style::default().fg(theme::TEXT)
};
spans.push(Span::styled(pattern_display, pattern_style));
spans.push(Span::styled(" Pattern: ", Style::default().fg(theme::DIM_TEXT)));

// Kit indicator (compact)
spans.push(Span::styled(
format!(" Kit[{}]", active_kit + 1),
if is_focused {
Style::default().fg(theme::AMBER)
// Pattern selector: q w e r t y u i o p
for (i, key) in PATTERN_KEYS.iter().enumerate() {
let style = if i == active_pattern {
Style::default().fg(Color::Black).bg(theme::CYAN).add_modifier(Modifier::BOLD)
} else if queued_pattern == Some(i) {
Style::default().fg(Color::Black).bg(theme::GOLD).add_modifier(Modifier::BOLD)
} else {
Style::default().fg(theme::TEXT)
},
));
Style::default().fg(theme::DIM_TEXT)
};
spans.push(Span::styled(*key, style));
if i < 9 { spans.push(Span::raw(" ")); }
}

spans.push(Span::styled(" \u{2502} Kit: ", Style::default().fg(theme::DIM_TEXT)));

// Kit selector: 1 2 3 4 5 6 7 8
for i in 0..8u8 {
let style = if i as usize == active_kit {
Style::default().fg(Color::Black).bg(theme::CYAN).add_modifier(Modifier::BOLD)
} else {
Style::default().fg(theme::DIM_TEXT)
};
spans.push(Span::styled(format!("{}", i + 1), style));
if i < 7 { spans.push(Span::raw(" ")); }
}

// Loop indicator
let loop_style = if loop_info.contains("--") {
Expand All @@ -254,7 +261,7 @@ fn status_line<'a>(
} else {
Style::default().fg(theme::TEXT)
};
spans.push(Span::styled(format!(" {}", loop_info), loop_style));
spans.push(Span::styled(format!(" {}", loop_info), loop_style));

Line::from(spans)
}
Loading