So much better
This commit is contained in:
@@ -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};
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user