diff --git a/src/input/mouse.rs b/src/input/mouse.rs index 1920f86..64f785f 100644 --- a/src/input/mouse.rs +++ b/src/input/mouse.rs @@ -4,7 +4,7 @@ use ratatui::layout::{Constraint, Layout, Rect}; use crate::commands::AppCommand; use crate::page::Page; use crate::state::{ - DeviceKind, DictFocus, EngineSection, HelpFocus, MainLayout, MinimapMode, Modal, OptionsFocus, + DeviceKind, DictFocus, EngineSection, HelpFocus, MinimapMode, Modal, OptionsFocus, PatternsColumn, SettingKind, }; 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 { + let h_pad = crate::views::horizontal_padding(term.width); Rect { - x: term.x + 4, + x: term.x + h_pad, y: term.y + 1, - width: term.width.saturating_sub(8), + width: term.width.saturating_sub(h_pad * 2), height: term.height.saturating_sub(2), } } @@ -303,111 +304,35 @@ fn handle_main_click(ctx: &mut InputContext, col: u16, row: u16, area: Rect) { return; } - // Replay viz/sequencer split - let show_scope = ctx.app.audio.config.show_scope; - let show_spectrum = ctx.app.audio.config.show_spectrum; - let show_preview = ctx.app.audio.config.show_preview; - let has_viz = show_scope || show_spectrum || show_preview; - let layout = ctx.app.audio.config.layout; - - 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 - } - }; + let pattern = ctx.app.current_edit_pattern(); + let has_viz = ctx.app.audio.config.show_scope + || ctx.app.audio.config.show_spectrum + || ctx.app.audio.config.show_preview; + let seq_h = main_view::sequencer_height(pattern.length, ctx.app.editor_ctx.step); + let (_, sequencer_area) = + main_view::viz_seq_split(main_area, ctx.app.audio.config.layout, has_viz, seq_h); if !contains(sequencer_area, col, row) { return; } - // Replay grid layout to find which step was clicked if let Some(step) = hit_test_grid(ctx, col, row, sequencer_area) { ctx.dispatch(AppCommand::GoToStep(step)); } } fn hit_test_grid(ctx: &InputContext, col: u16, row: u16, area: Rect) -> Option { - if area.width < 50 { - return None; - } - let pattern = ctx.app.current_edit_pattern(); let length = pattern.length; let page = ctx.app.editor_ctx.step / STEPS_PER_PAGE; let page_start = page * STEPS_PER_PAGE; let steps_on_page = (page_start + STEPS_PER_PAGE).min(length) - page_start; - let num_rows = steps_on_page.div_ceil(8); - let steps_per_row = steps_on_page.div_ceil(num_rows); - - let row_height = area.height / num_rows as u16; - - let row_constraints: Vec = (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 = (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 { - return Some(step_idx); - } + for (tile_rect, step_offset) in main_view::grid_layout(area, steps_on_page) { + if contains(tile_rect, col, row) { + let step_idx = page_start + step_offset; + if step_idx < length { + return Some(step_idx); } } } diff --git a/src/views/main_view.rs b/src/views/main_view.rs index 5cb35ee..a0f767c 100644 --- a/src/views/main_view.rs +++ b/src/views/main_view.rs @@ -27,52 +27,11 @@ pub fn layout(area: Rect) -> [Rect; 3] { pub fn render(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area: Rect) { let [main_area, _, vu_area] = layout(area); - let show_scope = app.audio.config.show_scope; - let show_spectrum = app.audio.config.show_spectrum; - let show_preview = app.audio.config.show_preview; - let has_viz = show_scope || show_spectrum || show_preview; - let layout = app.audio.config.layout; - - 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) - } - }; + let has_viz = app.audio.config.show_scope + || app.audio.config.show_spectrum + || app.audio.config.show_preview; + let seq_h = sequencer_height(app.current_edit_pattern().length, app.editor_ctx.step); + let (viz_area, sequencer_area) = viz_seq_split(main_area, app.audio.config.layout, has_viz, seq_h); if has_viz { render_viz_area(frame, app, snapshot, viz_area); @@ -128,6 +87,131 @@ fn render_viz_area( } 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 = 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 = 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) { let theme = theme::get(); @@ -146,43 +230,12 @@ fn render_sequencer(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, let page_start = page * STEPS_PER_PAGE; let steps_on_page = (page_start + STEPS_PER_PAGE).min(length) - page_start; - let num_rows = steps_on_page.div_ceil(8); - let steps_per_row = steps_on_page.div_ceil(num_rows); - - let row_height = area.height / num_rows as u16; - - let row_constraints: Vec = (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 = (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 { - render_tile(frame, cols[col_idx * 2], app, snapshot, step_idx); - } + for (tile_rect, step_offset) in grid_layout(area, steps_on_page) { + let step_idx = page_start + step_offset; + if step_idx < length { + render_tile(frame, tile_rect, app, snapshot, step_idx); } } - } fn render_tile( @@ -199,7 +252,9 @@ fn render_tile( 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_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)) .unwrap_or(false); @@ -228,7 +283,11 @@ fn render_tile( (Color::Rgb(r, g, b), theme.tile.active_fg) } (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) } (false, false, true, _, _) => (theme.selection.selected, theme.selection.cursor_fg), @@ -236,11 +295,8 @@ fn render_tile( (false, false, false, _, _) => (theme.tile.inactive_bg, theme.tile.inactive_fg), }; - 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 bg_fill = Paragraph::new("").style(Style::new().bg(bg)); + frame.render_widget(bg_fill, area); let source_idx = step.and_then(|s| s.source); let symbol = if is_playing { @@ -253,53 +309,54 @@ fn render_tile( format!("{:02}", step_idx + 1) }; - // For linked steps, get the name from the source step let step_name = if let Some(src) = source_idx { pattern.step(src as usize).and_then(|s| s.name.as_ref()) } else { 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 bg_fill = Paragraph::new("").style(Style::new().bg(bg)); - frame.render_widget(bg_fill, inner); + let center_y = area.y + area.height / 2; if let Some(name) = step_name { - let name_area = Rect { - x: inner.x, - y: inner.y + y_offset, - width: inner.width, - height: 1, - }; - let name_widget = Paragraph::new(name.as_str()) - .alignment(Alignment::Center) - .style(Style::new().bg(bg).fg(fg).add_modifier(Modifier::BOLD)); - frame.render_widget(name_widget, name_area); + if center_y > area.y { + let name_area = Rect { + x: area.x, + y: center_y - 1, + width: area.width, + height: 1, + }; + let name_widget = Paragraph::new(name.as_str()) + .alignment(Alignment::Center) + .style(Style::new().bg(bg).fg(fg).add_modifier(Modifier::BOLD)); + frame.render_widget(name_widget, name_area); + } + } - let symbol_area = Rect { - x: inner.x, - y: inner.y + y_offset + 1, - width: inner.width, - height: 1, - }; - let symbol_widget = Paragraph::new(symbol) - .alignment(Alignment::Center) - .style(Style::new().bg(bg).fg(fg).add_modifier(Modifier::BOLD)); - frame.render_widget(symbol_widget, symbol_area); - } else { - let centered_area = Rect { - x: inner.x, - y: inner.y + y_offset, - width: inner.width, - height: 1, - }; - let tile = Paragraph::new(symbol) - .alignment(Alignment::Center) - .style(Style::new().bg(bg).fg(fg).add_modifier(Modifier::BOLD)); - frame.render_widget(tile, centered_area); + let symbol_area = Rect { + x: area.x, + y: center_y, + width: area.width, + height: 1, + }; + let symbol_widget = Paragraph::new(symbol) + .alignment(Alignment::Center) + .style(Style::new().bg(bg).fg(fg).add_modifier(Modifier::BOLD)); + frame.render_widget(symbol_widget, symbol_area); + + if has_content && center_y + 1 < area.y + area.height { + let script = pattern.resolve_script(step_idx).unwrap_or(""); + if let Some(first_token) = script.split_whitespace().next() { + let hint_area = Rect { + x: area.x, + y: center_y + 1, + width: area.width, + height: 1, + }; + let hint_widget = Paragraph::new(first_token) + .alignment(Alignment::Center) + .style(Style::new().bg(bg).fg(theme.ui.text_dim)); + frame.render_widget(hint_widget, hint_area); + } } } diff --git a/src/views/mod.rs b/src/views/mod.rs index 37f31d1..0407e24 100644 --- a/src/views/mod.rs +++ b/src/views/mod.rs @@ -9,4 +9,4 @@ pub mod patterns_view; mod render; pub mod title_view; -pub use render::render; +pub use render::{horizontal_padding, render}; diff --git a/src/views/render.rs b/src/views/render.rs index d6290c4..eb66c0d 100644 --- a/src/views/render.rs +++ b/src/views/render.rs @@ -62,6 +62,16 @@ pub fn adjust_resolved_for_line( .collect() } +pub fn horizontal_padding(width: u16) -> u16 { + if width >= 120 { + 4 + } else if width >= 80 { + 2 + } else { + 1 + } +} + pub fn render( frame: &mut Frame, app: &App, @@ -90,10 +100,11 @@ pub fn render( return; } + let h_pad = horizontal_padding(term.width); let padded = Rect { - x: term.x + 4, + x: term.x + h_pad, y: term.y + 1, - width: term.width.saturating_sub(8), + width: term.width.saturating_sub(h_pad * 2), height: term.height.saturating_sub(2), };