Feat: UI/UX fixes + removing clones from places
This commit is contained in:
@@ -707,15 +707,6 @@ fn render_modal(
|
||||
.border_color(theme.modal.confirm)
|
||||
.render_centered(frame, term)
|
||||
}
|
||||
Modal::JumpToStep(input) => {
|
||||
let pattern_len = app.current_edit_pattern().length;
|
||||
let title = format!("Jump to Step (1-{})", pattern_len);
|
||||
TextInputModal::new(&title, input)
|
||||
.hint("Enter step number")
|
||||
.width(30)
|
||||
.border_color(theme.modal.confirm)
|
||||
.render_centered(frame, term)
|
||||
}
|
||||
Modal::SetTempo(input) => TextInputModal::new("Set Tempo (20-300 BPM)", input)
|
||||
.hint("Enter BPM")
|
||||
.width(30)
|
||||
@@ -883,6 +874,9 @@ fn render_modal(
|
||||
|
||||
inner
|
||||
}
|
||||
Modal::CommandPalette { input, cursor, scroll } => {
|
||||
render_command_palette(frame, app, input, *cursor, *scroll, term)
|
||||
}
|
||||
Modal::KeybindingsHelp { scroll } => render_modal_keybindings(frame, app, *scroll, term),
|
||||
Modal::EuclideanDistribution {
|
||||
source_step,
|
||||
@@ -1086,6 +1080,247 @@ fn render_modal_editor(
|
||||
inner
|
||||
}
|
||||
|
||||
fn render_command_palette(
|
||||
frame: &mut Frame,
|
||||
app: &App,
|
||||
query: &str,
|
||||
cursor: usize,
|
||||
scroll: usize,
|
||||
term: Rect,
|
||||
) -> Rect {
|
||||
use crate::model::palette::{palette_entries, CommandEntry};
|
||||
|
||||
let theme = theme::get();
|
||||
let entries = palette_entries(query, app.plugin_mode, app);
|
||||
|
||||
// On Main page, numeric input prepends a synthetic "Jump to Step" entry
|
||||
let jump_step: Option<usize> = if app.page == Page::Main
|
||||
&& !query.is_empty()
|
||||
&& query.chars().all(|c| c.is_ascii_digit())
|
||||
{
|
||||
query.parse().ok()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Build display rows: each is either a separator header or a command entry
|
||||
struct DisplayRow<'a> {
|
||||
entry: Option<&'a CommandEntry>,
|
||||
separator: Option<&'static str>,
|
||||
is_jump: bool,
|
||||
jump_label: String,
|
||||
}
|
||||
|
||||
let mut rows: Vec<DisplayRow> = Vec::new();
|
||||
|
||||
if let Some(n) = jump_step {
|
||||
rows.push(DisplayRow {
|
||||
entry: None,
|
||||
separator: None,
|
||||
is_jump: true,
|
||||
jump_label: format!("Jump to Step {n}"),
|
||||
});
|
||||
}
|
||||
|
||||
if query.is_empty() {
|
||||
// Grouped by category with separators
|
||||
let mut last_category = "";
|
||||
for e in &entries {
|
||||
if e.category != last_category {
|
||||
rows.push(DisplayRow {
|
||||
entry: None,
|
||||
separator: Some(e.category),
|
||||
is_jump: false,
|
||||
jump_label: String::new(),
|
||||
});
|
||||
last_category = e.category;
|
||||
}
|
||||
rows.push(DisplayRow {
|
||||
entry: Some(e),
|
||||
separator: None,
|
||||
is_jump: false,
|
||||
jump_label: String::new(),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
for e in &entries {
|
||||
rows.push(DisplayRow {
|
||||
entry: Some(e),
|
||||
separator: None,
|
||||
is_jump: false,
|
||||
jump_label: String::new(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Count selectable items (non-separator)
|
||||
let selectable_count = rows.iter().filter(|r| r.separator.is_none()).count();
|
||||
let cursor = cursor.min(selectable_count.saturating_sub(1));
|
||||
|
||||
let width: u16 = 55;
|
||||
let max_height = (term.height as usize * 60 / 100).max(8);
|
||||
let content_height = rows.len() + 4; // input + gap + hint + border padding
|
||||
let height = content_height.min(max_height) as u16;
|
||||
|
||||
let inner = ModalFrame::new(": Command Palette")
|
||||
.width(width)
|
||||
.height(height)
|
||||
.border_color(theme.modal.confirm)
|
||||
.render_centered(frame, term);
|
||||
|
||||
let mut y = inner.y;
|
||||
let content_width = inner.width;
|
||||
|
||||
// Input line
|
||||
let input_line = Line::from(vec![
|
||||
Span::styled("> ", Style::default().fg(theme.modal.confirm)),
|
||||
Span::styled(query, Style::default().fg(theme.ui.text_primary)),
|
||||
Span::styled("\u{2588}", Style::default().fg(theme.modal.confirm)),
|
||||
]);
|
||||
frame.render_widget(
|
||||
Paragraph::new(input_line),
|
||||
Rect::new(inner.x, y, content_width, 1),
|
||||
);
|
||||
y += 1;
|
||||
|
||||
// Visible area for entries
|
||||
let visible_height = inner.height.saturating_sub(2) as usize; // minus input line and hint line
|
||||
|
||||
// Auto-scroll
|
||||
let scroll = {
|
||||
let mut s = scroll;
|
||||
// Map cursor (selectable index) to row index for scrolling
|
||||
let mut selectable_idx = 0;
|
||||
let mut cursor_row = 0;
|
||||
for (i, row) in rows.iter().enumerate() {
|
||||
if row.separator.is_some() {
|
||||
continue;
|
||||
}
|
||||
if selectable_idx == cursor {
|
||||
cursor_row = i;
|
||||
break;
|
||||
}
|
||||
selectable_idx += 1;
|
||||
}
|
||||
if cursor_row >= s + visible_height {
|
||||
s = cursor_row + 1 - visible_height;
|
||||
}
|
||||
if cursor_row < s {
|
||||
s = cursor_row;
|
||||
}
|
||||
s
|
||||
};
|
||||
|
||||
// Render visible rows
|
||||
let mut selectable_idx = rows.iter().take(scroll).filter(|r| r.separator.is_none()).count();
|
||||
for row in rows.iter().skip(scroll).take(visible_height) {
|
||||
if y >= inner.y + inner.height - 1 {
|
||||
break;
|
||||
}
|
||||
|
||||
if let Some(cat) = row.separator {
|
||||
// Category header
|
||||
let pad = content_width.saturating_sub(cat.len() as u16 + 4) / 2;
|
||||
let sep_left = "\u{2500}".repeat(pad as usize);
|
||||
let sep_right =
|
||||
"\u{2500}".repeat(content_width.saturating_sub(pad + cat.len() as u16 + 4) as usize);
|
||||
let line = Line::from(vec![
|
||||
Span::styled(
|
||||
format!("{sep_left} "),
|
||||
Style::default().fg(theme.ui.text_muted),
|
||||
),
|
||||
Span::styled(cat, Style::default().fg(theme.ui.text_dim)),
|
||||
Span::styled(
|
||||
format!(" {sep_right}"),
|
||||
Style::default().fg(theme.ui.text_muted),
|
||||
),
|
||||
]);
|
||||
frame.render_widget(Paragraph::new(line), Rect::new(inner.x, y, content_width, 1));
|
||||
y += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
let is_selected = selectable_idx == cursor;
|
||||
let (bg, fg) = if is_selected {
|
||||
(theme.selection.cursor_bg, theme.selection.cursor_fg)
|
||||
} else if selectable_idx.is_multiple_of(2) {
|
||||
(theme.table.row_even, theme.ui.text_primary)
|
||||
} else {
|
||||
(theme.table.row_odd, theme.ui.text_primary)
|
||||
};
|
||||
|
||||
let (name, keybinding) = if row.is_jump {
|
||||
(row.jump_label.as_str(), "")
|
||||
} else if let Some(e) = row.entry {
|
||||
(e.name, e.keybinding)
|
||||
} else {
|
||||
selectable_idx += 1;
|
||||
y += 1;
|
||||
continue;
|
||||
};
|
||||
|
||||
let key_len = keybinding.len() as u16;
|
||||
let name_width = content_width.saturating_sub(key_len + 2);
|
||||
let truncated_name: String = name.chars().take(name_width as usize).collect();
|
||||
let padding = name_width.saturating_sub(truncated_name.len() as u16);
|
||||
|
||||
let key_fg = if is_selected {
|
||||
theme.selection.cursor_fg
|
||||
} else {
|
||||
theme.ui.text_dim
|
||||
};
|
||||
|
||||
let line = Line::from(vec![
|
||||
Span::styled(format!(" {truncated_name}"), Style::default().bg(bg).fg(fg)),
|
||||
Span::styled(
|
||||
" ".repeat(padding as usize),
|
||||
Style::default().bg(bg),
|
||||
),
|
||||
Span::styled(
|
||||
format!("{keybinding} "),
|
||||
Style::default().bg(bg).fg(key_fg),
|
||||
),
|
||||
]);
|
||||
frame.render_widget(Paragraph::new(line), Rect::new(inner.x, y, content_width, 1));
|
||||
|
||||
selectable_idx += 1;
|
||||
y += 1;
|
||||
}
|
||||
|
||||
// Empty state
|
||||
if selectable_count == 0 {
|
||||
let msg = "No matching commands";
|
||||
let empty_y = inner.y + inner.height / 2;
|
||||
if empty_y < inner.y + inner.height - 1 {
|
||||
frame.render_widget(
|
||||
Paragraph::new(msg)
|
||||
.style(Style::default().fg(theme.ui.text_muted))
|
||||
.alignment(Alignment::Center),
|
||||
Rect::new(inner.x, empty_y, content_width, 1),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Hint bar
|
||||
let hint_area = Rect::new(inner.x, inner.y + inner.height - 1, inner.width, 1);
|
||||
let hints = if jump_step.is_some() && cursor == 0 {
|
||||
hint_line(&[
|
||||
("\u{2191}\u{2193}", "navigate"),
|
||||
("Enter", "jump to step"),
|
||||
("Esc", "close"),
|
||||
])
|
||||
} else {
|
||||
hint_line(&[
|
||||
("\u{2191}\u{2193}", "navigate"),
|
||||
("Enter", "run"),
|
||||
("Esc", "close"),
|
||||
])
|
||||
};
|
||||
frame.render_widget(Paragraph::new(hints), hint_area);
|
||||
|
||||
inner
|
||||
}
|
||||
|
||||
fn render_modal_keybindings(frame: &mut Frame, app: &App, scroll: usize, term: Rect) -> Rect {
|
||||
let theme = theme::get();
|
||||
let width = (term.width * 80 / 100).clamp(60, 100);
|
||||
@@ -1098,7 +1333,7 @@ fn render_modal_keybindings(frame: &mut Frame, app: &App, scroll: usize, term: R
|
||||
.border_color(theme.modal.editor)
|
||||
.render_centered(frame, term);
|
||||
|
||||
let bindings = super::keybindings::bindings_for(app.page, app.plugin_mode);
|
||||
let bindings = crate::model::palette::bindings_for(app.page, app.plugin_mode);
|
||||
let visible_rows = inner.height.saturating_sub(2) as usize;
|
||||
|
||||
let rows: Vec<Row> = bindings
|
||||
|
||||
Reference in New Issue
Block a user