So much better

This commit is contained in:
2026-01-26 02:24:04 +01:00
parent bde64e7dc5
commit 1b32a91b0d
16 changed files with 714 additions and 135 deletions

View File

@@ -1,10 +1,10 @@
mod file;
mod project;
pub const MAX_BANKS: usize = 16;
pub const MAX_PATTERNS: usize = 16;
pub const MAX_BANKS: usize = 32;
pub const MAX_PATTERNS: usize = 32;
pub const MAX_STEPS: usize = 128;
pub const DEFAULT_LENGTH: usize = 16;
pub use file::{load, save, FileError};
pub use project::{Bank, Pattern, PatternSpeed, Project, Step};
pub use project::{Bank, LaunchQuantization, Pattern, PatternSpeed, Project, Step, SyncMode};

View File

@@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize};
use crate::{DEFAULT_LENGTH, MAX_BANKS, MAX_PATTERNS, MAX_STEPS};
#[derive(Clone, Copy, Serialize, Deserialize, Default, PartialEq)]
#[derive(Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
pub enum PatternSpeed {
Eighth, // 1/8x
Quarter, // 1/4x
@@ -79,6 +79,75 @@ impl PatternSpeed {
}
}
#[derive(Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
pub enum LaunchQuantization {
Immediate,
Beat,
#[default]
Bar,
Bars2,
Bars4,
Bars8,
}
impl LaunchQuantization {
pub fn label(&self) -> &'static str {
match self {
Self::Immediate => "Immediate",
Self::Beat => "Beat",
Self::Bar => "1 Bar",
Self::Bars2 => "2 Bars",
Self::Bars4 => "4 Bars",
Self::Bars8 => "8 Bars",
}
}
pub fn next(&self) -> Self {
match self {
Self::Immediate => Self::Beat,
Self::Beat => Self::Bar,
Self::Bar => Self::Bars2,
Self::Bars2 => Self::Bars4,
Self::Bars4 => Self::Bars8,
Self::Bars8 => Self::Bars8,
}
}
pub fn prev(&self) -> Self {
match self {
Self::Immediate => Self::Immediate,
Self::Beat => Self::Immediate,
Self::Bar => Self::Beat,
Self::Bars2 => Self::Bar,
Self::Bars4 => Self::Bars2,
Self::Bars8 => Self::Bars4,
}
}
}
#[derive(Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
pub enum SyncMode {
#[default]
Reset,
PhaseLock,
}
impl SyncMode {
pub fn label(&self) -> &'static str {
match self {
Self::Reset => "Reset",
Self::PhaseLock => "Phase-Lock",
}
}
pub fn toggle(&self) -> Self {
match self {
Self::Reset => Self::PhaseLock,
Self::PhaseLock => Self::Reset,
}
}
}
#[derive(Clone, Serialize, Deserialize)]
pub struct Step {
pub active: bool,
@@ -108,6 +177,10 @@ pub struct Pattern {
pub speed: PatternSpeed,
#[serde(default)]
pub name: Option<String>,
#[serde(default)]
pub quantization: LaunchQuantization,
#[serde(default)]
pub sync_mode: SyncMode,
}
impl Default for Pattern {
@@ -117,6 +190,8 @@ impl Default for Pattern {
length: DEFAULT_LENGTH,
speed: PatternSpeed::default(),
name: None,
quantization: LaunchQuantization::default(),
sync_mode: SyncMode::default(),
}
}
}

View File

@@ -60,6 +60,40 @@ pub struct Editor {
search: SearchState,
}
impl Editor {
pub fn start_selection(&mut self) {
self.text.start_selection();
}
pub fn cancel_selection(&mut self) {
self.text.cancel_selection();
}
pub fn is_selecting(&self) -> bool {
self.text.is_selecting()
}
pub fn copy(&mut self) {
self.text.copy();
}
pub fn cut(&mut self) -> bool {
self.text.cut()
}
pub fn paste(&mut self) -> bool {
self.text.paste()
}
pub fn select_all(&mut self) {
self.text.select_all();
}
pub fn selection_range(&self) -> Option<((usize, usize), (usize, usize))> {
self.text.selection_range()
}
}
impl Default for Editor {
fn default() -> Self {
Self::new()
@@ -300,6 +334,9 @@ impl Editor {
pub fn render(&self, frame: &mut Frame, area: Rect, highlighter: Highlighter) {
let (cursor_row, cursor_col) = self.text.cursor();
let cursor_style = Style::default().bg(Color::White).fg(Color::Black);
let selection_style = Style::default().bg(Color::Rgb(60, 80, 120));
let selection = self.text.selection_range();
let lines: Vec<Line> = self
.text
@@ -309,39 +346,30 @@ impl Editor {
.map(|(row, line)| {
let tokens = highlighter(row, line);
let mut spans: Vec<Span> = Vec::new();
let mut col = 0;
if row == cursor_row {
let mut col = 0;
for (style, text) in tokens {
let text_len = text.chars().count();
if cursor_col >= col && cursor_col < col + text_len {
let before = text.chars().take(cursor_col - col).collect::<String>();
let cursor_char =
text.chars().nth(cursor_col - col).unwrap_or(' ');
let after =
text.chars().skip(cursor_col - col + 1).collect::<String>();
for (base_style, text) in tokens {
for ch in text.chars() {
let is_cursor = row == cursor_row && col == cursor_col;
let is_selected = is_in_selection(row, col, selection);
if !before.is_empty() {
spans.push(Span::styled(before, style));
}
spans.push(Span::styled(cursor_char.to_string(), cursor_style));
if !after.is_empty() {
spans.push(Span::styled(after, style));
}
let style = if is_cursor {
cursor_style
} else if is_selected {
base_style.bg(selection_style.bg.unwrap())
} else {
spans.push(Span::styled(text, style));
}
col += text_len;
}
if cursor_col >= col {
spans.push(Span::styled(" ", cursor_style));
}
} else {
for (style, text) in tokens {
spans.push(Span::styled(text, style));
base_style
};
spans.push(Span::styled(ch.to_string(), style));
col += 1;
}
}
if row == cursor_row && cursor_col >= col {
spans.push(Span::styled(" ", cursor_style));
}
Line::from(spans)
})
.collect();
@@ -468,3 +496,23 @@ impl Editor {
fn is_word_char(c: char) -> bool {
c.is_alphanumeric() || matches!(c, '!' | '@' | '?' | '.' | ':' | '_' | '#')
}
fn is_in_selection(row: usize, col: usize, selection: Option<((usize, usize), (usize, usize))>) -> bool {
let Some(((start_row, start_col), (end_row, end_col))) = selection else {
return false;
};
if row < start_row || row > end_row {
return false;
}
if row == start_row && row == end_row {
col >= start_col && col < end_col
} else if row == start_row {
col >= start_col
} else if row == end_row {
col < end_col
} else {
true
}
}