mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-06 11:46:41 +01:00
ItemPalette
This commit is contained in:
parent
fa8316c651
commit
042d480b67
12 changed files with 505 additions and 424 deletions
|
|
@ -75,7 +75,7 @@ impl<T: HasPhrases> Command<T> for PhrasePoolCommand {
|
|||
Some(Self::SetLength(index, old_len))
|
||||
},
|
||||
SetColor(index, color) => {
|
||||
let mut color = ItemColorTriplet::from(color);
|
||||
let mut color = ItemPalette::from(color);
|
||||
std::mem::swap(&mut color, &mut model.phrases()[index].write().unwrap().color);
|
||||
Some(Self::SetColor(index, color.base))
|
||||
},
|
||||
|
|
@ -104,7 +104,7 @@ pub struct Phrase {
|
|||
/// All notes are displayed with minimum length
|
||||
pub percussive: bool,
|
||||
/// Identifying color of phrase
|
||||
pub color: ItemColorTriplet,
|
||||
pub color: ItemPalette,
|
||||
}
|
||||
|
||||
/// MIDI message structural
|
||||
|
|
@ -116,7 +116,7 @@ impl Phrase {
|
|||
loop_on: bool,
|
||||
length: usize,
|
||||
notes: Option<PhraseData>,
|
||||
color: Option<ItemColorTriplet>,
|
||||
color: Option<ItemPalette>,
|
||||
) -> Self {
|
||||
Self {
|
||||
uuid: uuid::Uuid::new_v4(),
|
||||
|
|
@ -128,7 +128,7 @@ impl Phrase {
|
|||
loop_start: 0,
|
||||
loop_length: length,
|
||||
percussive: true,
|
||||
color: color.unwrap_or_else(ItemColorTriplet::random)
|
||||
color: color.unwrap_or_else(ItemPalette::random)
|
||||
}
|
||||
}
|
||||
pub fn set_length (&mut self, length: usize) {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ pub struct ItemColor {
|
|||
}
|
||||
/// A color in OKHSL and RGB with lighter and darker variants.
|
||||
#[derive(Debug, Default, Copy, Clone, PartialEq)]
|
||||
pub struct ItemColorTriplet {
|
||||
pub struct ItemPalette {
|
||||
pub base: ItemColor,
|
||||
pub light: ItemColor,
|
||||
pub dark: ItemColor,
|
||||
|
|
@ -44,7 +44,7 @@ impl ItemColor {
|
|||
self.okhsl.mix(other.okhsl, distance).into()
|
||||
}
|
||||
}
|
||||
impl From<ItemColor> for ItemColorTriplet {
|
||||
impl From<ItemColor> for ItemPalette {
|
||||
fn from (base: ItemColor) -> Self {
|
||||
let mut light = base.okhsl.clone();
|
||||
light.lightness = (light.lightness * 1.15).min(Okhsl::<f32>::max_lightness());
|
||||
|
|
@ -54,7 +54,7 @@ impl From<ItemColor> for ItemColorTriplet {
|
|||
Self { base, light: light.into(), dark: dark.into() }
|
||||
}
|
||||
}
|
||||
impl ItemColorTriplet {
|
||||
impl ItemPalette {
|
||||
pub fn random () -> Self {
|
||||
ItemColor::random().into()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -110,7 +110,10 @@ pub trait Area<N: Coordinate>: Copy {
|
|||
[self.x(), self.y(), a, self.h()],
|
||||
[self.x() + a, self.y(), self.w().minus(a), self.h()],
|
||||
),
|
||||
_ => todo!(),
|
||||
Direction::Left => (
|
||||
[self.x() + self.w() - a, self.y(), a, self.h()],
|
||||
[self.x(), self.y(), self.w() - a, self.h()],
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
use crate::*;
|
||||
|
||||
/// A widget that tracks its render width and height
|
||||
pub struct Measure<E: Engine>(PhantomData<E>, AtomicUsize, AtomicUsize);
|
||||
#[derive(Default)]
|
||||
pub struct Measure<E: Engine>(PhantomData<E>, AtomicUsize, AtomicUsize, bool);
|
||||
|
||||
impl<E: Engine> Clone for Measure<E> {
|
||||
fn clone (&self) -> Self {
|
||||
|
|
@ -9,6 +10,7 @@ impl<E: Engine> Clone for Measure<E> {
|
|||
Default::default(),
|
||||
AtomicUsize::from(self.1.load(Ordering::Relaxed)),
|
||||
AtomicUsize::from(self.2.load(Ordering::Relaxed)),
|
||||
self.3
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -16,8 +18,8 @@ impl<E: Engine> Clone for Measure<E> {
|
|||
impl<E: Engine> std::fmt::Debug for Measure<E> {
|
||||
fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
|
||||
f.debug_struct("Measure")
|
||||
.field("width", &self.0)
|
||||
.field("height", &self.1)
|
||||
.field("width", &self.1)
|
||||
.field("height", &self.2)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
|
@ -29,18 +31,24 @@ impl<E: Engine> Measure<E> {
|
|||
pub fn set_w (&self, w: impl Into<usize>) { self.1.store(w.into(), Ordering::Relaxed) }
|
||||
pub fn set_h (&self, h: impl Into<usize>) { self.2.store(h.into(), Ordering::Relaxed) }
|
||||
pub fn set_wh (&self, w: impl Into<usize>, h: impl Into<usize>) { self.set_w(w); self.set_h(h); }
|
||||
pub fn new () -> Self { Self(PhantomData::default(), 0.into(), 0.into()) }
|
||||
pub fn new () -> Self { Self(PhantomData::default(), 0.into(), 0.into(), false) }
|
||||
pub fn debug () -> Self { Self(PhantomData::default(), 0.into(), 0.into(), true) }
|
||||
pub fn format (&self) -> String { format!("{}x{}", self.w(), self.h()) }
|
||||
}
|
||||
|
||||
impl<E: Engine> Render<E> for Measure<E> {
|
||||
fn min_size (&self, _: E::Size) -> Perhaps<E::Size> {
|
||||
impl Render<Tui> for Measure<Tui> {
|
||||
fn min_size (&self, _: [u16;2]) -> Perhaps<[u16;2]> {
|
||||
Ok(Some([0u16.into(), 0u16.into()].into()))
|
||||
}
|
||||
fn render (&self, to: &mut E::Output) -> Usually<()> {
|
||||
self.set_w(to.area().w());
|
||||
self.set_h(to.area().h());
|
||||
Ok(())
|
||||
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
|
||||
let w = to.area().w();
|
||||
self.set_w(w);
|
||||
let h = to.area().h();
|
||||
self.set_h(h);
|
||||
Ok(if self.3 {
|
||||
to.blit(&format!(" {w} x {h} "), to.area.x(), to.area.y(), Some(
|
||||
Style::default().bold().italic().bg(Color::Rgb(255, 0, 255)).fg(Color::Rgb(0,0,0))
|
||||
))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,41 +4,52 @@ impl<E: Engine> LayoutSplit<E> for E {}
|
|||
|
||||
pub trait LayoutSplit<E: Engine> {
|
||||
fn split <A: Render<E>, B: Render<E>> (
|
||||
direction: Direction, amount: E::Unit, a: A, b: B
|
||||
flip: bool, direction: Direction, amount: E::Unit, a: A, b: B
|
||||
) -> Split<E, A, B> {
|
||||
Split::new(direction, amount, a, b)
|
||||
Split::new(flip, direction, amount, a, b)
|
||||
}
|
||||
fn split_up <A: Render<E>, B: Render<E>> (
|
||||
amount: E::Unit, a: A, b: B
|
||||
flip: bool, amount: E::Unit, a: A, b: B
|
||||
) -> Split<E, A, B> {
|
||||
Split::new(Direction::Up, amount, a, b)
|
||||
Self::split(flip, Direction::Up, amount, a, b)
|
||||
}
|
||||
fn split_down <A: Render<E>, B: Render<E>> (
|
||||
flip: bool, amount: E::Unit, a: A, b: B
|
||||
) -> Split<E, A, B> {
|
||||
Self::split(flip, Direction::Down, amount, a, b)
|
||||
}
|
||||
fn split_left <A: Render<E>, B: Render<E>> (
|
||||
flip: bool, amount: E::Unit, a: A, b: B
|
||||
) -> Split<E, A, B> {
|
||||
Self::split(flip, Direction::Left, amount, a, b)
|
||||
}
|
||||
fn split_right <A: Render<E>, B: Render<E>> (
|
||||
flip: bool, amount: E::Unit, a: A, b: B
|
||||
) -> Split<E, A, B> {
|
||||
Self::split(flip, Direction::Right, amount, a, b)
|
||||
}
|
||||
|
||||
//fn split_flip <W: Render<E>> (
|
||||
//self, direction: Direction, amount: E::Unit, other: W
|
||||
//) -> Split<E, W, Self> { Split::new(direction, amount, other, self) }
|
||||
}
|
||||
|
||||
/// A binary split with fixed proportion
|
||||
pub struct Split<E: Engine, A: Render<E>, B: Render<E>>(
|
||||
pub Direction, pub E::Unit, A, B, PhantomData<E>
|
||||
pub bool, pub Direction, pub E::Unit, A, B, PhantomData<E>
|
||||
);
|
||||
|
||||
impl<E: Engine, A: Render<E>, B: Render<E>> Split<E, A, B> {
|
||||
pub fn new (direction: Direction, proportion: E::Unit, a: A, b: B) -> Self {
|
||||
Self(direction, proportion, a, b, Default::default())
|
||||
pub fn new (flip: bool, direction: Direction, proportion: E::Unit, a: A, b: B) -> Self {
|
||||
Self(flip, direction, proportion, a, b, Default::default())
|
||||
}
|
||||
pub fn up (proportion: E::Unit, a: A, b: B) -> Self {
|
||||
Self(Direction::Up, proportion, a, b, Default::default())
|
||||
pub fn up (flip: bool, proportion: E::Unit, a: A, b: B) -> Self {
|
||||
Self(flip, Direction::Up, proportion, a, b, Default::default())
|
||||
}
|
||||
pub fn down (proportion: E::Unit, a: A, b: B) -> Self {
|
||||
Self(Direction::Down, proportion, a, b, Default::default())
|
||||
pub fn down (flip: bool, proportion: E::Unit, a: A, b: B) -> Self {
|
||||
Self(flip, Direction::Down, proportion, a, b, Default::default())
|
||||
}
|
||||
pub fn left (proportion: E::Unit, a: A, b: B) -> Self {
|
||||
Self(Direction::Left, proportion, a, b, Default::default())
|
||||
pub fn left (flip: bool, proportion: E::Unit, a: A, b: B) -> Self {
|
||||
Self(flip, Direction::Left, proportion, a, b, Default::default())
|
||||
}
|
||||
pub fn right (proportion: E::Unit, a: A, b: B) -> Self {
|
||||
Self(Direction::Right, proportion, a, b, Default::default())
|
||||
pub fn right (flip: bool, proportion: E::Unit, a: A, b: B) -> Self {
|
||||
Self(flip, Direction::Right, proportion, a, b, Default::default())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -47,9 +58,13 @@ impl<E: Engine, A: Render<E>, B: Render<E>> Render<E> for Split<E, A, B> {
|
|||
Ok(Some(to))
|
||||
}
|
||||
fn render (&self, to: &mut E::Output) -> Usually<()> {
|
||||
let (a, b) = to.area().split_fixed(self.0, self.1);
|
||||
to.render_in(a.into(), &self.2)?;
|
||||
to.render_in(b.into(), &self.3)?;
|
||||
Ok(())
|
||||
let (a, b) = to.area().split_fixed(self.1, self.2);
|
||||
Ok(if self.0 {
|
||||
to.render_in(a.into(), &self.4)?;
|
||||
to.render_in(b.into(), &self.3)?;
|
||||
} else {
|
||||
to.render_in(a.into(), &self.3)?;
|
||||
to.render_in(b.into(), &self.4)?;
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -147,6 +147,7 @@ render!(|self: ArrangerTui|{
|
|||
))
|
||||
])),
|
||||
Split::right(
|
||||
false,
|
||||
self.splits[1],
|
||||
PhraseListView::from(self),
|
||||
PhraseView::from(self),
|
||||
|
|
@ -174,6 +175,18 @@ impl HasPhrases for ArrangerTui {
|
|||
}
|
||||
}
|
||||
|
||||
impl HasEditor for ArrangerTui {
|
||||
fn editor (&self) -> &PhraseEditorModel {
|
||||
&self.editor
|
||||
}
|
||||
fn editor_focused (&self) -> bool {
|
||||
self.focused() == ArrangerFocus::PhraseEditor
|
||||
}
|
||||
fn editor_entered (&self) -> bool {
|
||||
self.entered() && self.editor_focused()
|
||||
}
|
||||
}
|
||||
|
||||
/// Sections in the arranger app that may be focused
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
|
||||
pub enum ArrangerFocus {
|
||||
|
|
@ -1173,7 +1186,7 @@ impl ArrangerControl for ArrangerTui {
|
|||
},
|
||||
ArrangerSelection::Clip(t, s) => {
|
||||
if let Some(phrase) = &self.scenes_mut()[s].clips[t] {
|
||||
phrase.write().unwrap().color = ItemColorTriplet::random();
|
||||
phrase.write().unwrap().color = ItemPalette::random();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
0
crates/tek/src/tui/app_groovebox.rs
Normal file
0
crates/tek/src/tui/app_groovebox.rs
Normal file
|
|
@ -41,7 +41,7 @@ impl TryFrom<&Arc<RwLock<JackClient>>> for SequencerTui {
|
|||
midi_buf: vec![vec![];65536],
|
||||
note_buf: vec![],
|
||||
perf: PerfModel::default(),
|
||||
focus: FocusState::Focused(SequencerFocus::PhraseEditor)
|
||||
focus: FocusState::Entered(SequencerFocus::PhraseEditor)
|
||||
})
|
||||
|
||||
}
|
||||
|
|
@ -107,9 +107,9 @@ impl Audio for SequencerTui {
|
|||
}
|
||||
}
|
||||
|
||||
render!(|self: SequencerTui|lay!([self.size, Tui::split_up(1,
|
||||
Tui::fill_xy(Tui::at_s(SequencerStatusBar::from(self))),
|
||||
Tui::split_up(2,
|
||||
render!(|self: SequencerTui|lay!([self.size, Tui::split_up(false, 1,
|
||||
Tui::fill_xy(SequencerStatusBar::from(self)),
|
||||
Tui::split_down(false, 2,
|
||||
TransportView::from((
|
||||
self,
|
||||
self.player.play_phrase().as_ref().map(|(_,p)|p.as_ref().map(|p|p.read().unwrap().color)).flatten(),
|
||||
|
|
@ -119,28 +119,22 @@ render!(|self: SequencerTui|lay!([self.size, Tui::split_up(1,
|
|||
false
|
||||
}
|
||||
)),
|
||||
row!([
|
||||
Tui::fixed_x(20, Tui::split_up(4, col!([
|
||||
PhraseSelector::next_phrase(
|
||||
&self.player,
|
||||
self.focused() == SequencerFocus::PhraseNext,
|
||||
self.entered()
|
||||
),
|
||||
Tui::split_left(false, 20,
|
||||
Tui::fixed_x(20, Tui::split_down(false, 4, col!([
|
||||
PhraseSelector::play_phrase(
|
||||
&self.player,
|
||||
self.focused() == SequencerFocus::PhrasePlay,
|
||||
self.entered()
|
||||
&self.player, self.focused() == SequencerFocus::PhrasePlay, self.entered()
|
||||
),
|
||||
]), col!([
|
||||
PhraseSelector::next_phrase(
|
||||
&self.player, self.focused() == SequencerFocus::PhraseNext, self.entered()
|
||||
),
|
||||
]), Tui::split_up(false, 2,
|
||||
PhraseSelector::edit_phrase(
|
||||
&self.editor.phrase,
|
||||
self.focused() == SequencerFocus::PhraseEditor,
|
||||
self.entered()
|
||||
&self.editor.phrase, self.focused() == SequencerFocus::PhraseEditor, self.entered()
|
||||
),
|
||||
PhraseListView::from(self),
|
||||
]))),
|
||||
))),
|
||||
PhraseView::from(self)
|
||||
])
|
||||
)
|
||||
)
|
||||
)]));
|
||||
|
||||
|
|
@ -159,6 +153,18 @@ impl HasPhrases for SequencerTui {
|
|||
}
|
||||
}
|
||||
|
||||
impl HasEditor for SequencerTui {
|
||||
fn editor (&self) -> &PhraseEditorModel {
|
||||
&self.editor
|
||||
}
|
||||
fn editor_focused (&self) -> bool {
|
||||
self.focused() == SequencerFocus::PhraseEditor
|
||||
}
|
||||
fn editor_entered (&self) -> bool {
|
||||
self.entered() && self.editor_focused()
|
||||
}
|
||||
}
|
||||
|
||||
/// Sections in the sequencer app that may be focused
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
|
||||
pub enum SequencerFocus {
|
||||
|
|
@ -176,7 +182,7 @@ pub enum SequencerFocus {
|
|||
impl From<&SequencerTui> for Option<TransportFocus> {
|
||||
fn from (state: &SequencerTui) -> Self {
|
||||
match state.focus.inner() {
|
||||
SequencerFocus::Transport(focus) => Some(focus),
|
||||
Transport(focus) => Some(focus),
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
|
|
@ -219,7 +225,7 @@ impl_focus!(SequencerTui SequencerFocus [
|
|||
PhraseEditor,
|
||||
],
|
||||
] => [self: {
|
||||
if self.focus.is_entered() && self.focus.inner() == SequencerFocus::PhraseEditor {
|
||||
if self.focus.is_entered() && self.focus.inner() == PhraseEditor {
|
||||
self.editor.edit_mode = PhraseEditMode::Note
|
||||
} else {
|
||||
self.editor.edit_mode = PhraseEditMode::Scroll
|
||||
|
|
@ -403,9 +409,9 @@ pub enum SequencerCommand {
|
|||
Phrases(PhrasesCommand),
|
||||
Editor(PhraseCommand),
|
||||
Enqueue(Option<Arc<RwLock<Phrase>>>),
|
||||
Clear,
|
||||
Undo,
|
||||
Redo,
|
||||
//Clear,
|
||||
//Undo,
|
||||
//Redo,
|
||||
}
|
||||
|
||||
impl Command<SequencerTui> for SequencerCommand {
|
||||
|
|
@ -419,9 +425,9 @@ impl Command<SequencerTui> for SequencerCommand {
|
|||
state.player.enqueue_next(phrase.as_ref());
|
||||
None
|
||||
},
|
||||
Self::Undo => { todo!() },
|
||||
Self::Redo => { todo!() },
|
||||
Self::Clear => { todo!() },
|
||||
//Self::Undo => { todo!() },
|
||||
//Self::Redo => { todo!() },
|
||||
//Self::Clear => { todo!() },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -453,37 +459,34 @@ impl InputToCommand<Tui, SequencerTui> for SequencerCommand {
|
|||
}
|
||||
|
||||
pub fn to_sequencer_command (state: &SequencerTui, input: &TuiInput) -> Option<SequencerCommand> {
|
||||
let stopped = state.clock().is_stopped();
|
||||
Some(match input.event() {
|
||||
// Play/pause
|
||||
key!(Char(' ')) => Clock(
|
||||
if state.clock().is_stopped() { Play(None) } else { Pause(None) }
|
||||
),
|
||||
key!(Char(' ')) => Clock(if stopped { Play(None) } else { Pause(None) }),
|
||||
// Play from start/rewind to start
|
||||
key!(Shift-Char(' ')) => Clock(
|
||||
if state.clock().is_stopped() { Play(Some(0)) } else { Pause(Some(0)) }
|
||||
),
|
||||
key!(Shift-Char(' ')) => Clock(if stopped { Play(Some(0)) } else { Pause(Some(0)) }),
|
||||
// Edit phrase
|
||||
key!(Char('e')) => match state.focused() {
|
||||
SequencerFocus::PhrasePlay => Editor(Show(
|
||||
PhrasePlay => Editor(Show(
|
||||
state.player.play_phrase().as_ref().map(|x|x.1.as_ref()).flatten().map(|x|x.clone())
|
||||
)),
|
||||
SequencerFocus::PhraseNext => Editor(Show(
|
||||
PhraseNext => Editor(Show(
|
||||
state.player.next_phrase().as_ref().map(|x|x.1.as_ref()).flatten().map(|x|x.clone())
|
||||
)),
|
||||
SequencerFocus::PhraseList => Editor(Show(
|
||||
PhraseList => Editor(Show(
|
||||
Some(state.phrases.phrases[state.phrases.phrase.load(Ordering::Relaxed)].clone())
|
||||
)),
|
||||
_ => return None,
|
||||
},
|
||||
_ => match state.focused() {
|
||||
SequencerFocus::Transport(_) => match TransportCommand::input_to_command(state, input)? {
|
||||
Transport(_) => match TransportCommand::input_to_command(state, input)? {
|
||||
TransportCommand::Clock(command) => Clock(command),
|
||||
_ => return None,
|
||||
},
|
||||
SequencerFocus::PhraseEditor => Editor(
|
||||
PhraseEditor => Editor(
|
||||
PhraseCommand::input_to_command(&state.editor, input)?
|
||||
),
|
||||
SequencerFocus::PhraseList => match input.event() {
|
||||
PhraseList => match input.event() {
|
||||
key!(Enter) => Enqueue(Some(
|
||||
state.phrases.phrases[state.phrases.phrase.load(Ordering::Relaxed)].clone()
|
||||
)),
|
||||
|
|
|
|||
|
|
@ -75,13 +75,13 @@ pub struct TransportView {
|
|||
current_second: f64,
|
||||
}
|
||||
|
||||
impl<T: HasClock> From<(&T, Option<ItemColorTriplet>, bool)> for TransportView {
|
||||
fn from ((state, color, focused): (&T, Option<ItemColorTriplet>, bool)) -> Self {
|
||||
impl<T: HasClock> From<(&T, Option<ItemPalette>, bool)> for TransportView {
|
||||
fn from ((state, color, focused): (&T, Option<ItemPalette>, bool)) -> Self {
|
||||
let clock = state.clock();
|
||||
let sr = format!("{:.1}k", clock.timebase.sr.get() / 1000.0);
|
||||
let bpm = format!("{:.3}", clock.timebase.bpm.get());
|
||||
let ppq = format!("{:.0}", clock.timebase.ppq.get());
|
||||
let color = color.unwrap_or(ItemColorTriplet::from(ItemColor::from(TuiTheme::g(100))));
|
||||
let color = color.unwrap_or(ItemPalette::from(ItemColor::from(TuiTheme::g(100))));
|
||||
let bg = if focused { color.light.rgb } else { color.dark.rgb };
|
||||
if let Some(started) = clock.started.read().unwrap().as_ref() {
|
||||
let current_sample = (clock.global.sample.get() - started.sample.get())/1000.;
|
||||
|
|
@ -114,7 +114,7 @@ impl<T: HasClock> From<(&T, Option<ItemColorTriplet>, bool)> for TransportView {
|
|||
global_second: format!("{:.1}s", clock.global.usec.get()/1000000.),
|
||||
current_sample: 0.0,
|
||||
current_second: 0.0,
|
||||
beat: format!("0.0.00")
|
||||
beat: format!("000.0.00")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -133,31 +133,39 @@ render!(|self: TransportView|{
|
|||
let border_style = Style::default()
|
||||
.bg(bg)
|
||||
.fg(TuiTheme::border_fg(self.focused));
|
||||
let play_bg = if self.started{Color::Rgb(0,128,0)}else{Color::Rgb(128,64,0)};
|
||||
Tui::bg(bg, lay!(move|add|{
|
||||
add(&Tui::fill_x(Tui::at_w(lay!(move|add|{
|
||||
add(&Lozenge(border_style))?;
|
||||
add(&Tui::outset_x(1, row!([
|
||||
TransportField("Beat", self.beat.as_str()), " ",
|
||||
TransportField("BPM ", self.bpm.as_str()), " ",
|
||||
])))
|
||||
}))))?;
|
||||
add(&Tui::fill_x(Tui::center_x(Tui::pull_x(2, row!([
|
||||
col!(|add|{
|
||||
if self.started {
|
||||
add(&col!([Tui::fg(Color::Rgb(0, 255, 0), "▶ PLAYING"), ""]))
|
||||
} else {
|
||||
add(&col!(["", Tui::fg(Color::Rgb(255, 128, 0), "⏹ STOPPED")]))
|
||||
}
|
||||
}),
|
||||
])))))?;
|
||||
add(&Tui::fill_x(Tui::at_e(lay!(move|add|{
|
||||
add(&Lozenge(border_style))?;
|
||||
add(&Tui::outset_x(1, row!([
|
||||
TransportField("Second", format!("{:.1}s", self.current_second).as_str()), " ",
|
||||
TransportField("Rate ", self.sr.as_str()), " ",
|
||||
TransportField("Sample", format!("{:.0}k", self.current_sample).as_str()),
|
||||
])))
|
||||
}))))
|
||||
add(&Tui::fill_x(Tui::at_w(lay!([
|
||||
//Lozenge(border_style),
|
||||
Tui::outset_x(0, row!([
|
||||
Tui::bg(play_bg, Tui::outset_x(1, Tui::fixed_x(9, col!(|add|{
|
||||
if self.started {
|
||||
add(&col!([Tui::fg(Color::Rgb(0, 255, 0), "▶ PLAYING"), ""]))
|
||||
} else {
|
||||
add(&col!(["", Tui::fg(Color::Rgb(255, 128, 0), "⏹ STOPPED")]))
|
||||
}
|
||||
})))), " ",
|
||||
Tui::fixed_x(10, TransportField("Beat",
|
||||
self.beat.as_str())), " ",
|
||||
Tui::fixed_x(8, TransportField("BPM ",
|
||||
self.bpm.as_str())), " ",
|
||||
Tui::fixed_x(8, TransportField("Second",
|
||||
format!("{:.1}s", self.current_second).as_str())), " ",
|
||||
Tui::fixed_x(8, TransportField("Rate ",
|
||||
self.sr.as_str())), " ",
|
||||
Tui::fixed_x(8, TransportField("Sample",
|
||||
format!("{:.0}k", self.current_sample).as_str())),
|
||||
]))
|
||||
]))))?;
|
||||
//add(&Tui::fill_x(Tui::center_x(Tui::pull_x(2, row!([
|
||||
//<center>
|
||||
//])))))?;
|
||||
//add(&Tui::fill_x(Tui::at_e(lay!(move|add|{
|
||||
////add(&Lozenge(border_style))?;
|
||||
//add(&Tui::outset_x(1, row!([
|
||||
//])))
|
||||
//}))))
|
||||
Ok(())
|
||||
}))
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,9 @@ pub trait Theme {
|
|||
fn null () -> Color {
|
||||
Color::Reset
|
||||
}
|
||||
fn bg0 () -> Color {
|
||||
Color::Rgb(20, 20, 20)
|
||||
}
|
||||
fn bg () -> Color {
|
||||
Color::Rgb(28, 35, 25)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,17 @@
|
|||
use crate::*;
|
||||
|
||||
pub trait HasEditor {
|
||||
fn editor (&self) -> &PhraseEditorModel;
|
||||
fn editor_focused (&self) -> bool;
|
||||
fn editor_entered (&self) -> bool;
|
||||
}
|
||||
|
||||
/// Contains state for viewing and editing a phrase
|
||||
pub struct PhraseEditorModel {
|
||||
/// Phrase being played
|
||||
pub(crate) phrase: Option<Arc<RwLock<Phrase>>>,
|
||||
/// Length of note that will be inserted, in pulses
|
||||
pub(crate) note_len: usize,
|
||||
/// The full piano roll is rendered to this buffer
|
||||
pub(crate) buffer: BigBuffer,
|
||||
/// Notes currently held at input
|
||||
pub(crate) notes_in: Arc<RwLock<[bool; 128]>>,
|
||||
/// Notes currently held at output
|
||||
|
|
@ -24,25 +28,7 @@ pub struct PhraseEditorModel {
|
|||
pub(crate) time_point: AtomicUsize,
|
||||
|
||||
pub(crate) edit_mode: PhraseEditMode,
|
||||
pub(crate) view_mode: PhraseViewMode,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum PhraseEditMode {
|
||||
Note,
|
||||
Scroll,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum PhraseViewMode {
|
||||
PianoHorizontal {
|
||||
time_zoom: usize,
|
||||
note_zoom: PhraseViewNoteZoom,
|
||||
},
|
||||
PianoVertical {
|
||||
time_zoom: usize,
|
||||
note_zoom: PhraseViewNoteZoom,
|
||||
},
|
||||
pub(crate) view_mode: Box<dyn PhraseViewMode + Send + Sync>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for PhraseEditorModel {
|
||||
|
|
@ -65,7 +51,6 @@ impl Default for PhraseEditorModel {
|
|||
Self {
|
||||
phrase: None,
|
||||
note_len: 24,
|
||||
buffer: Default::default(),
|
||||
notes_in: RwLock::new([false;128]).into(),
|
||||
notes_out: RwLock::new([false;128]).into(),
|
||||
now: Pulse::default().into(),
|
||||
|
|
@ -75,10 +60,11 @@ impl Default for PhraseEditorModel {
|
|||
note_point: 0.into(),
|
||||
time_start: 0.into(),
|
||||
time_point: 0.into(),
|
||||
view_mode: PhraseViewMode::PianoHorizontal {
|
||||
view_mode: Box::new(PianoHorizontal {
|
||||
buffer: Default::default(),
|
||||
time_zoom: 24,
|
||||
note_zoom: PhraseViewNoteZoom::N(1)
|
||||
},
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -96,7 +82,7 @@ impl PhraseEditorModel {
|
|||
let end = (start + self.note_len) % phrase.length;
|
||||
phrase.notes[time].push(MidiMessage::NoteOn { key, vel });
|
||||
phrase.notes[end].push(MidiMessage::NoteOff { key, vel });
|
||||
self.buffer = self.view_mode.draw(&phrase);
|
||||
self.view_mode.show(Some(&phrase), self.note_len);
|
||||
}
|
||||
}
|
||||
/// Move time cursor forward by current note length
|
||||
|
|
@ -109,61 +95,43 @@ impl PhraseEditorModel {
|
|||
/// Select which pattern to display. This pre-renders it to the buffer at full resolution.
|
||||
pub fn show_phrase (&mut self, phrase: Option<Arc<RwLock<Phrase>>>) {
|
||||
if phrase.is_some() {
|
||||
self.buffer = self.view_mode.draw(&*phrase.as_ref().unwrap().read().unwrap());
|
||||
self.phrase = phrase;
|
||||
let phrase = &*self.phrase.as_ref().unwrap().read().unwrap();
|
||||
self.view_mode.show(Some(&phrase), self.note_len);
|
||||
} else {
|
||||
self.buffer = Default::default();
|
||||
self.view_mode.show(None, self.note_len);
|
||||
self.phrase = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait HasEditor {
|
||||
fn editor (&self) -> &PhraseEditorModel;
|
||||
fn editor_focused (&self) -> bool;
|
||||
fn editor_entered (&self) -> bool;
|
||||
}
|
||||
|
||||
impl HasEditor for SequencerTui {
|
||||
fn editor (&self) -> &PhraseEditorModel {
|
||||
&self.editor
|
||||
}
|
||||
fn editor_focused (&self) -> bool {
|
||||
self.focused() == SequencerFocus::PhraseEditor
|
||||
}
|
||||
fn editor_entered (&self) -> bool {
|
||||
self.entered() && self.editor_focused()
|
||||
}
|
||||
}
|
||||
|
||||
impl HasEditor for ArrangerTui {
|
||||
fn editor (&self) -> &PhraseEditorModel {
|
||||
&self.editor
|
||||
}
|
||||
fn editor_focused (&self) -> bool {
|
||||
self.focused() == ArrangerFocus::PhraseEditor
|
||||
}
|
||||
fn editor_entered (&self) -> bool {
|
||||
self.entered() && self.editor_focused()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PhraseView<'a> {
|
||||
focused: bool,
|
||||
entered: bool,
|
||||
phrase: &'a Option<Arc<RwLock<Phrase>>>,
|
||||
buffer: &'a BigBuffer,
|
||||
note_len: usize,
|
||||
now: &'a Arc<Pulse>,
|
||||
size: &'a Measure<Tui>,
|
||||
view_mode: &'a PhraseViewMode,
|
||||
note_point: usize,
|
||||
note_range: (usize, usize),
|
||||
note_names: (&'a str, &'a str),
|
||||
time_start: usize,
|
||||
time_point: usize,
|
||||
note_len: usize,
|
||||
phrase: &'a Option<Arc<RwLock<Phrase>>>,
|
||||
view_mode: &'a Box<dyn PhraseViewMode + Send + Sync>,
|
||||
now: &'a Arc<Pulse>,
|
||||
size: &'a Measure<Tui>,
|
||||
focused: bool,
|
||||
entered: bool,
|
||||
}
|
||||
|
||||
render!(|self: PhraseView<'a>|{
|
||||
let bg = if self.focused { TuiTheme::g(32) } else { Color::Reset };
|
||||
let fg = self.phrase.as_ref()
|
||||
.map(|p|p.read().unwrap().color.clone())
|
||||
.unwrap_or(ItemPalette::from(ItemColor::from(TuiTheme::g(64))));
|
||||
Tui::bg(bg, lay!([
|
||||
PhraseTimeline(&self, fg),
|
||||
PhraseViewNotes(&self, fg),
|
||||
PhraseViewCursor(&self),
|
||||
PhraseViewKeys(&self, fg),
|
||||
PhraseViewStats(&self, fg),
|
||||
//Measure::debug(),
|
||||
]))
|
||||
});
|
||||
impl<'a, T: HasEditor> From<&'a T> for PhraseView<'a> {
|
||||
fn from (state: &'a T) -> Self {
|
||||
let editor = state.editor();
|
||||
|
|
@ -181,88 +149,118 @@ impl<'a, T: HasEditor> From<&'a T> for PhraseView<'a> {
|
|||
editor.note_lo.store(note_lo, Ordering::Relaxed);
|
||||
}
|
||||
Self {
|
||||
focused: state.editor_focused(),
|
||||
entered: state.editor_entered(),
|
||||
note_len: editor.note_len,
|
||||
phrase: &editor.phrase,
|
||||
buffer: &editor.buffer,
|
||||
now: &editor.now,
|
||||
size: &editor.size,
|
||||
view_mode: &editor.view_mode,
|
||||
|
||||
note_point,
|
||||
note_range: (note_lo, note_hi),
|
||||
note_names: (to_note_name(note_lo), to_note_name(note_hi)),
|
||||
|
||||
time_start: editor.time_start.load(Ordering::Relaxed),
|
||||
time_point: editor.time_point.load(Ordering::Relaxed),
|
||||
note_len: editor.note_len,
|
||||
phrase: &editor.phrase,
|
||||
view_mode: &editor.view_mode,
|
||||
size: &editor.size,
|
||||
now: &editor.now,
|
||||
focused: state.editor_focused(),
|
||||
entered: state.editor_entered(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render!(|self: PhraseView<'a>|{
|
||||
let Self {
|
||||
focused, entered, size,
|
||||
phrase, view_mode, buffer,
|
||||
note_point, note_len,
|
||||
note_range: (note_lo, note_hi),
|
||||
time_start, time_point,
|
||||
//now: _,
|
||||
..
|
||||
} = self;
|
||||
let phrase_color = phrase.as_ref().map(|p|p.read().unwrap().color.clone())
|
||||
.unwrap_or(ItemColorTriplet::from(ItemColor::from(TuiTheme::g(64))));
|
||||
let title_color = if *focused{phrase_color.light.rgb}else{phrase_color.dark.rgb};
|
||||
let bg = if self.focused { TuiTheme::bg() } else { Color::Reset };
|
||||
pub struct PhraseTimeline<'a>(&'a PhraseView<'a>, ItemPalette);
|
||||
render!(|self: PhraseTimeline<'a>|Tui::fg(TuiTheme::g(224), Tui::push_x(5, format!("|000.00.00"))));
|
||||
|
||||
pub struct PhraseViewStats<'a>(&'a PhraseView<'a>, ItemPalette);
|
||||
render!(|self: PhraseViewStats<'a>|{
|
||||
let title_color = if self.0.focused{self.1.light.rgb}else{self.1.dark.rgb};
|
||||
lay!([
|
||||
Tui::bg(bg, Tui::inset_x(1, Tui::fill_x(row!([
|
||||
Tui::push_y(1, Tui::fill_y(Widget::new(|to:[u16;2]|Ok(Some(to.clip_w(5))), move|to: &mut TuiOutput|{
|
||||
Ok(if to.area().h() >= 2 { view_mode.render_keys(to, *note_hi, *note_lo) })
|
||||
}))),
|
||||
Tui::fill_x(lay!([
|
||||
Tui::push_y(1, Tui::fill_x(Widget::new(|to|Ok(Some(to)), |to: &mut TuiOutput|{
|
||||
size.set_wh(to.area.w(), to.area.h() as usize - 1);
|
||||
let draw = to.area().h() >= 2;
|
||||
Ok(if draw { view_mode.render_notes(to, buffer, *time_start, *note_hi) })
|
||||
}))),
|
||||
Tui::push_y(1, Widget::new(|to|Ok(Some(to)), move|to: &mut TuiOutput|{
|
||||
Ok(if *focused && *entered {
|
||||
view_mode.render_cursor(
|
||||
to,
|
||||
*time_point, *time_start, view_mode.time_zoom(),
|
||||
*note_point, *note_len, *note_hi, *note_lo,
|
||||
)
|
||||
})
|
||||
}))
|
||||
])),
|
||||
])))),
|
||||
lay!(move|add|{
|
||||
add(&Lozenge(Style::default().bg(phrase_color.base.rgb).fg(phrase_color.base.rgb)))?;
|
||||
let upper_left = format!("{}",
|
||||
phrase.as_ref().map(|p|p.read().unwrap().name.clone()).unwrap_or(String::new())
|
||||
);
|
||||
let lower_left = format!("");
|
||||
let mut lower_right = format!(" {} ", size.format());
|
||||
if *focused && *entered {
|
||||
lower_right = format!("Note: {} ({}) {} {lower_right}",
|
||||
note_point, to_note_name(*note_point), pulses_to_name(*note_len)
|
||||
Tui::at_sw({
|
||||
let mut lower_right = format!(" {} ", self.0.size.format());
|
||||
if self.0.focused && self.0.entered {
|
||||
lower_right = format!(
|
||||
"Note: {} ({}) {} {lower_right}",
|
||||
self.0.note_point,
|
||||
to_note_name(self.0.note_point),
|
||||
pulses_to_name(self.0.note_len),
|
||||
);
|
||||
}
|
||||
let mut upper_right = format!("[{}]", if *entered {"■"} else {" "});
|
||||
if let Some(phrase) = phrase {
|
||||
upper_right = format!("Time: {}/{} {} {upper_right}",
|
||||
time_point, phrase.read().unwrap().length, pulses_to_name(view_mode.time_zoom()),
|
||||
Tui::bg(title_color, Tui::fg(TuiTheme::g(224), lower_right))
|
||||
}),
|
||||
Tui::fill_xy(Tui::at_se({
|
||||
let mut upper_right = format!("[{}]", if self.0.entered {"■"} else {" "});
|
||||
if let Some(phrase) = self.0.phrase {
|
||||
upper_right = format!(
|
||||
"Time: {}/{} {} {upper_right}",
|
||||
self.0.time_point,
|
||||
phrase.read().unwrap().length,
|
||||
pulses_to_name(self.0.view_mode.time_zoom()),
|
||||
)
|
||||
};
|
||||
add(&Tui::push_x(1, Tui::at_nw(Tui::fg(TuiTheme::g(224), Tui::bg(title_color, upper_left)))))?;
|
||||
add(&Tui::at_sw(Tui::bg(title_color, Tui::fg(TuiTheme::g(224), lower_left))))?;
|
||||
add(&Tui::fill_xy(Tui::at_ne(Tui::pull_x(1, Tui::bg(title_color, Tui::fg(TuiTheme::g(224), upper_right))))))?;
|
||||
add(&Tui::fill_xy(Tui::at_se(Tui::pull_x(1, Tui::bg(title_color, Tui::fg(TuiTheme::g(224), lower_right))))))?;
|
||||
Ok(())
|
||||
}),
|
||||
Tui::pull_x(1, Tui::bg(title_color, Tui::fg(TuiTheme::g(224), upper_right)))
|
||||
})),
|
||||
])
|
||||
});
|
||||
|
||||
struct PhraseViewKeys<'a>(&'a PhraseView<'a>, ItemPalette);
|
||||
render!(|self: PhraseViewKeys<'a>|{
|
||||
let layout = |to:[u16;2]|Ok(Some(to.clip_w(5)));
|
||||
Tui::fill_xy(Widget::new(layout, |to: &mut TuiOutput|Ok(
|
||||
self.0.view_mode.render_keys(to, self.1.dark.rgb, Some(self.0.note_point), self.0.note_range)
|
||||
)))
|
||||
});
|
||||
|
||||
struct PhraseViewNotes<'a>(&'a PhraseView<'a>, ItemPalette);
|
||||
render!(|self: PhraseViewNotes<'a>|Tui::fill_xy(render(|to: &mut TuiOutput|{
|
||||
self.0.size.set_wh(to.area.w(), to.area.h() as usize);
|
||||
Ok(self.0.view_mode.render_notes(to, self.0.time_start, self.0.note_range.1))
|
||||
})));
|
||||
|
||||
struct PhraseViewCursor<'a>(&'a PhraseView<'a>);
|
||||
render!(|self: PhraseViewCursor<'a>|Tui::fill_xy(render(|to: &mut TuiOutput|Ok(
|
||||
self.0.view_mode.render_cursor(
|
||||
to,
|
||||
self.0.time_point,
|
||||
self.0.time_start,
|
||||
self.0.view_mode.time_zoom(),
|
||||
self.0.note_point,
|
||||
self.0.note_len,
|
||||
self.0.note_range.1,
|
||||
self.0.note_range.0,
|
||||
)
|
||||
))));
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum PhraseEditMode {
|
||||
Note,
|
||||
Scroll,
|
||||
}
|
||||
|
||||
pub trait PhraseViewMode {
|
||||
fn show (&mut self, phrase: Option<&Phrase>, note_len: usize);
|
||||
fn time_zoom (&self) -> usize;
|
||||
fn set_time_zoom (&mut self, time_zoom: usize);
|
||||
fn buffer_width (&self, phrase: &Phrase) -> usize;
|
||||
fn buffer_height (&self, phrase: &Phrase) -> usize;
|
||||
fn render_keys (&self,
|
||||
to: &mut TuiOutput, color: Color, point: Option<usize>, range: (usize, usize));
|
||||
fn render_notes (&self,
|
||||
to: &mut TuiOutput, time_start: usize, note_hi: usize);
|
||||
fn render_cursor (
|
||||
&self,
|
||||
to: &mut TuiOutput,
|
||||
time_point: usize,
|
||||
time_start: usize,
|
||||
time_zoom: usize,
|
||||
note_point: usize,
|
||||
note_len: usize,
|
||||
note_hi: usize,
|
||||
note_lo: usize,
|
||||
);
|
||||
}
|
||||
|
||||
pub struct PianoHorizontal {
|
||||
time_zoom: usize,
|
||||
note_zoom: PhraseViewNoteZoom,
|
||||
buffer: BigBuffer,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum PhraseViewNoteZoom {
|
||||
N(usize),
|
||||
|
|
@ -270,97 +268,100 @@ pub enum PhraseViewNoteZoom {
|
|||
Octant,
|
||||
}
|
||||
|
||||
impl PhraseViewMode {
|
||||
pub fn time_zoom (&self) -> usize {
|
||||
match self {
|
||||
Self::PianoHorizontal { time_zoom, .. } => *time_zoom,
|
||||
impl PhraseViewMode for PianoHorizontal {
|
||||
fn time_zoom (&self) -> usize {
|
||||
self.time_zoom
|
||||
}
|
||||
fn set_time_zoom (&mut self, time_zoom: usize) {
|
||||
self.time_zoom = time_zoom
|
||||
}
|
||||
fn show (&mut self, phrase: Option<&Phrase>, note_len: usize) {
|
||||
if let Some(phrase) = phrase {
|
||||
self.buffer = BigBuffer::new(self.buffer_width(phrase), self.buffer_height(phrase));
|
||||
draw_piano_horizontal(&mut self.buffer, phrase, self.time_zoom, note_len);
|
||||
} else {
|
||||
self.buffer = Default::default();
|
||||
}
|
||||
}
|
||||
fn buffer_width (&self, phrase: &Phrase) -> usize {
|
||||
phrase.length / self.time_zoom
|
||||
}
|
||||
/// Determine the required height to render the phrase.
|
||||
fn buffer_height (&self, phrase: &Phrase) -> usize {
|
||||
match self.note_zoom {
|
||||
PhraseViewNoteZoom::Half => 64,
|
||||
PhraseViewNoteZoom::N(n) => 128*n,
|
||||
_ => unimplemented!()
|
||||
}
|
||||
}
|
||||
pub fn set_time_zoom (&mut self, time_zoom: usize) {
|
||||
*self = match self {
|
||||
Self::PianoHorizontal { note_zoom, .. } => Self::PianoHorizontal {
|
||||
note_zoom: *note_zoom,
|
||||
time_zoom,
|
||||
},
|
||||
_ => unimplemented!()
|
||||
}
|
||||
}
|
||||
/// Return a new [BigBuffer] containing a render of the phrase.
|
||||
pub fn draw (&self, phrase: &Phrase) -> BigBuffer {
|
||||
let mut buffer = BigBuffer::new(self.buffer_width(phrase), self.buffer_height(phrase));
|
||||
match self {
|
||||
Self::PianoHorizontal { time_zoom, note_zoom } => match note_zoom {
|
||||
PhraseViewNoteZoom::N(_) => Self::draw_piano_horizontal(
|
||||
&mut buffer, phrase, *time_zoom
|
||||
),
|
||||
_ => unimplemented!(),
|
||||
},
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
buffer
|
||||
}
|
||||
/// Draw a subsection of the [BigBuffer] onto a regular ratatui [Buffer].
|
||||
fn render_notes (
|
||||
&self,
|
||||
target: &mut TuiOutput,
|
||||
source: &BigBuffer,
|
||||
target: &mut TuiOutput,
|
||||
time_start: usize,
|
||||
note_hi: usize,
|
||||
) {
|
||||
let area = target.area();
|
||||
let [x0, y0, w, h] = target.area().xywh();
|
||||
let source = &self.buffer;
|
||||
let target = &mut target.buffer;
|
||||
match self {
|
||||
Self::PianoHorizontal { .. } => {
|
||||
let [x0, y0, w, h] = area.xywh();
|
||||
for (x, target_x) in (x0..x0+w).enumerate() {
|
||||
for (y, target_y) in (y0..y0+h).enumerate() {
|
||||
if y > note_hi {
|
||||
break
|
||||
}
|
||||
let source_x = time_start + x;
|
||||
let source_y = note_hi - y;
|
||||
// TODO: enable loop rollover:
|
||||
//let source_x = (time_start + x) % source.width.max(1);
|
||||
//let source_y = (note_hi - y) % source.height.max(1);
|
||||
if source_x < source.width && source_y < source.height {
|
||||
let target_cell = target.get_mut(target_x, target_y);
|
||||
if let Some(source_cell) = source.get(source_x, source_y) {
|
||||
*target_cell = source_cell.clone();
|
||||
}
|
||||
}
|
||||
for (x, target_x) in (x0..x0+w).enumerate() {
|
||||
|
||||
for (y, target_y) in (y0..y0+h).enumerate() {
|
||||
|
||||
if y > note_hi {
|
||||
break
|
||||
}
|
||||
|
||||
let source_x = time_start + x;
|
||||
let source_y = note_hi - y;
|
||||
|
||||
// TODO: enable loop rollover:
|
||||
//let source_x = (time_start + x) % source.width.max(1);
|
||||
//let source_y = (note_hi - y) % source.height.max(1);
|
||||
if source_x < source.width && source_y < source.height {
|
||||
let target_cell = target.get_mut(target_x, target_y);
|
||||
if let Some(source_cell) = source.get(source_x, source_y) {
|
||||
*target_cell = source_cell.clone();
|
||||
}
|
||||
}
|
||||
},
|
||||
_ => unimplemented!()
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
fn render_keys (&self, to: &mut TuiOutput, note_hi: usize, note_lo: usize) {
|
||||
let style = Some(Style::default().fg(Color::Rgb(192, 192, 192)).bg(Color::Rgb(0, 0, 0)));
|
||||
match self {
|
||||
Self::PianoHorizontal { .. } => {
|
||||
let [x0, y0, _, _] = to.area().xywh();
|
||||
for (y, note) in (note_lo..=note_hi).rev().enumerate() {
|
||||
let key = match note % 12 {
|
||||
11 => "█████",
|
||||
10 => " ",
|
||||
9 => "█████",
|
||||
8 => " ",
|
||||
7 => "█████",
|
||||
6 => " ",
|
||||
5 => "█████",
|
||||
4 => "█████",
|
||||
3 => " ",
|
||||
2 => "█████",
|
||||
1 => " ",
|
||||
0 => "█████",
|
||||
_ => unreachable!(),
|
||||
};
|
||||
to.blit(&key, x0, y0 + y as u16, style);
|
||||
to.blit(&format!("{}", to_note_name(note)), x0, y0 + y as u16, None);
|
||||
}
|
||||
},
|
||||
_ => unimplemented!()
|
||||
fn render_keys (
|
||||
&self,
|
||||
to: &mut TuiOutput,
|
||||
color: Color,
|
||||
point: Option<usize>,
|
||||
(note_lo, note_hi): (usize, usize)
|
||||
) {
|
||||
let [x, y0, _, _] = to.area().xywh();
|
||||
let key_style = Some(Style::default().fg(Color::Rgb(192, 192, 192)).bg(Color::Rgb(0, 0, 0)));
|
||||
let note_off_style = Some(Style::default().fg(TuiTheme::g(160)));
|
||||
let note_on_style = Some(Style::default().fg(TuiTheme::g(255)).bg(color).bold());
|
||||
for (y, note) in (note_lo..=note_hi).rev().enumerate().map(|(y, n)|(y0 + y as u16, n)) {
|
||||
let key = match note % 12 {
|
||||
11 => "████▌",
|
||||
10 => " ",
|
||||
9 => "████▌",
|
||||
8 => " ",
|
||||
7 => "████▌",
|
||||
6 => " ",
|
||||
5 => "████▌",
|
||||
4 => "████▌",
|
||||
3 => " ",
|
||||
2 => "████▌",
|
||||
1 => " ",
|
||||
0 => "████▌",
|
||||
_ => unreachable!(),
|
||||
};
|
||||
to.blit(&key, x, y, key_style);
|
||||
|
||||
if Some(note) == point {
|
||||
to.blit(&format!("{:<5}", to_note_name(note)), x, y, note_on_style)
|
||||
} else {
|
||||
to.blit(&to_note_name(note), x, y, note_off_style)
|
||||
};
|
||||
}
|
||||
}
|
||||
fn render_cursor (
|
||||
|
|
@ -374,121 +375,147 @@ impl PhraseViewMode {
|
|||
note_hi: usize,
|
||||
note_lo: usize,
|
||||
) {
|
||||
let [x0, y0, w, _] = to.area().xywh();
|
||||
let style = Some(Style::default().fg(Color::Rgb(0,255,0)));
|
||||
match self {
|
||||
Self::PianoHorizontal { .. } => {
|
||||
let [x0, y0, w, _] = to.area().xywh();
|
||||
for (y, note) in (note_lo..=note_hi).rev().enumerate() {
|
||||
if note == note_point {
|
||||
for x in 0..w {
|
||||
let time_1 = time_start + x as usize * time_zoom;
|
||||
let time_2 = time_1 + time_zoom;
|
||||
if time_1 <= time_point && time_point < time_2 {
|
||||
to.blit(&"█", x0 + x as u16, y0 + y as u16, style);
|
||||
let tail = note_len as u16 / time_zoom as u16;
|
||||
for x_tail in (x0 + x + 1)..(x0 + x + tail) {
|
||||
to.blit(&"▄", x_tail, y0 + y as u16, style);
|
||||
}
|
||||
break
|
||||
}
|
||||
for (y, note) in (note_lo..=note_hi).rev().enumerate() {
|
||||
if note == note_point {
|
||||
for x in 0..w {
|
||||
let time_1 = time_start + x as usize * time_zoom;
|
||||
let time_2 = time_1 + time_zoom;
|
||||
if time_1 <= time_point && time_point < time_2 {
|
||||
to.blit(&"█", x0 + x as u16, y0 + y as u16, style);
|
||||
let tail = note_len as u16 / time_zoom as u16;
|
||||
for x_tail in (x0 + x + 1)..(x0 + x + tail) {
|
||||
to.blit(&"▄", x_tail, y0 + y as u16, style);
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
},
|
||||
_ => unimplemented!()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Determine the required width to render the phrase.
|
||||
fn buffer_width (&self, phrase: &Phrase) -> usize {
|
||||
match self {
|
||||
Self::PianoHorizontal { time_zoom, .. } => {
|
||||
phrase.length / time_zoom
|
||||
},
|
||||
Self::PianoVertical { note_zoom, .. } => match note_zoom {
|
||||
PhraseViewNoteZoom::Half => 64,
|
||||
PhraseViewNoteZoom::N(n) => 128*n,
|
||||
_ => unimplemented!()
|
||||
},
|
||||
}
|
||||
|
||||
fn draw_piano_horizontal_bg (
|
||||
target: &mut BigBuffer,
|
||||
phrase: &Phrase,
|
||||
time_zoom: usize,
|
||||
note_len: usize,
|
||||
) {
|
||||
for (y, note) in (0..127).rev().enumerate() {
|
||||
for (x, time) in (0..target.width).map(|x|(x, x*time_zoom)) {
|
||||
let cell = target.get_mut(x, y).unwrap();
|
||||
//cell.set_fg(Color::Rgb(48, 55, 45));
|
||||
cell.set_fg(phrase.color.dark.rgb);
|
||||
cell.set_char(if time % 384 == 0 {
|
||||
'│'
|
||||
} else if time % 96 == 0 {
|
||||
'╎'
|
||||
} else if time % note_len == 0 {
|
||||
'┊'
|
||||
} else if (127 - note) % 12 == 1 {
|
||||
'='
|
||||
} else {
|
||||
'·'
|
||||
});
|
||||
}
|
||||
}
|
||||
/// Determine the required height to render the phrase.
|
||||
fn buffer_height (&self, phrase: &Phrase) -> usize {
|
||||
match self {
|
||||
Self::PianoHorizontal { note_zoom, .. } => match note_zoom {
|
||||
PhraseViewNoteZoom::Half => 64,
|
||||
PhraseViewNoteZoom::N(n) => 128*n,
|
||||
_ => unimplemented!()
|
||||
},
|
||||
Self::PianoVertical { time_zoom, .. } => {
|
||||
phrase.length / time_zoom
|
||||
},
|
||||
}
|
||||
}
|
||||
/// Draw the piano roll using full blocks on note on and half blocks on legato: █▄ █▄ █▄
|
||||
fn draw_piano_horizontal (
|
||||
target: &mut BigBuffer,
|
||||
phrase: &Phrase,
|
||||
time_zoom: usize,
|
||||
) {
|
||||
let color = phrase.color.light.rgb;
|
||||
let style = Style::default().fg(color);//.bg(Color::Rgb(0, 0, 0));
|
||||
}
|
||||
|
||||
fn draw_piano_horizontal_fg (
|
||||
target: &mut BigBuffer,
|
||||
phrase: &Phrase,
|
||||
time_zoom: usize,
|
||||
) {
|
||||
let style = Style::default().fg(phrase.color.light.rgb);//.bg(Color::Rgb(0, 0, 0));
|
||||
let mut notes_on = [false;128];
|
||||
for (x, time_start) in (0..phrase.length).step_by(time_zoom).enumerate() {
|
||||
|
||||
for (y, note) in (0..127).rev().enumerate() {
|
||||
for (x, time) in (0..target.width).map(|x|(x, x*time_zoom)) {
|
||||
let cell = target.get_mut(x, y).unwrap();
|
||||
//cell.set_fg(Color::Rgb(48, 55, 45));
|
||||
cell.set_fg(phrase.color.dark.rgb);
|
||||
cell.set_char(if time % 384 == 0 {
|
||||
'│'
|
||||
} else if time % 96 == 0 {
|
||||
'╎'
|
||||
} else if note % 12 == 0 {
|
||||
'='
|
||||
} else {
|
||||
'·'
|
||||
});
|
||||
let cell = target.get_mut(x, note).unwrap();
|
||||
if notes_on[note] {
|
||||
cell.set_char('▄');
|
||||
cell.set_style(style);
|
||||
}
|
||||
}
|
||||
let mut notes_on = [false;128];
|
||||
for (x, time_start) in (0..phrase.length).step_by(time_zoom).enumerate() {
|
||||
let time_end = time_start + time_zoom;
|
||||
for (y, note) in (0..127).rev().enumerate() {
|
||||
let cell = target.get_mut(x, note).unwrap();
|
||||
if notes_on[note] {
|
||||
cell.set_char('▄');
|
||||
cell.set_style(style);
|
||||
}
|
||||
}
|
||||
for time in time_start..time_end {
|
||||
for event in phrase.notes[time].iter() {
|
||||
match event {
|
||||
MidiMessage::NoteOn { key, .. } => {
|
||||
let note = key.as_int() as usize;
|
||||
let cell = target.get_mut(x, note).unwrap();
|
||||
cell.set_char('█');
|
||||
cell.set_style(style);
|
||||
notes_on[note] = true
|
||||
},
|
||||
MidiMessage::NoteOff { key, .. } => {
|
||||
notes_on[key.as_int() as usize] = false
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let time_end = time_start + time_zoom;
|
||||
for time in time_start..time_end {
|
||||
for event in phrase.notes[time].iter() {
|
||||
match event {
|
||||
MidiMessage::NoteOn { key, .. } => {
|
||||
let note = key.as_int() as usize;
|
||||
let cell = target.get_mut(x, note).unwrap();
|
||||
cell.set_char('█');
|
||||
cell.set_style(style);
|
||||
notes_on[note] = true
|
||||
},
|
||||
MidiMessage::NoteOff { key, .. } => {
|
||||
notes_on[key.as_int() as usize] = false
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
/// TODO: Draw the piano roll using octant blocks (U+1CD00-U+1CDE5)
|
||||
fn draw_piano_horizontal_octant (
|
||||
_: &mut BigBuffer, _: &Phrase, _: usize
|
||||
) {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
/// Draw the piano roll using full blocks on note on and half blocks on legato: █▄ █▄ █▄
|
||||
fn draw_piano_horizontal (
|
||||
target: &mut BigBuffer,
|
||||
phrase: &Phrase,
|
||||
time_zoom: usize,
|
||||
note_len: usize,
|
||||
) {
|
||||
let color = phrase.color.light.rgb;
|
||||
let style = Style::default().fg(color);//.bg(Color::Rgb(0, 0, 0));
|
||||
for (y, note) in (0..127).rev().enumerate() {
|
||||
for (x, time) in (0..target.width).map(|x|(x, x*time_zoom)) {
|
||||
let cell = target.get_mut(x, y).unwrap();
|
||||
//cell.set_fg(Color::Rgb(48, 55, 45));
|
||||
cell.set_fg(phrase.color.dark.rgb);
|
||||
cell.set_char(if time % 384 == 0 {
|
||||
'│'
|
||||
} else if time % 96 == 0 {
|
||||
'╎'
|
||||
} else if time % note_len == 0 {
|
||||
'┊'
|
||||
} else if (127 - note) % 12 == 1 {
|
||||
'='
|
||||
} else {
|
||||
'·'
|
||||
});
|
||||
}
|
||||
}
|
||||
/// TODO: Draw the piano roll using half blocks: ▄▀▄
|
||||
fn draw_piano_horizontal_half (
|
||||
_: &mut BigBuffer, _: &Phrase, _: usize
|
||||
) {
|
||||
unimplemented!()
|
||||
let mut notes_on = [false;128];
|
||||
for (x, time_start) in (0..phrase.length).step_by(time_zoom).enumerate() {
|
||||
let time_end = time_start + time_zoom;
|
||||
for (y, note) in (0..127).rev().enumerate() {
|
||||
let cell = target.get_mut(x, note).unwrap();
|
||||
if notes_on[note] {
|
||||
cell.set_char('▄');
|
||||
cell.set_style(style);
|
||||
}
|
||||
}
|
||||
for time in time_start..time_end {
|
||||
for event in phrase.notes[time].iter() {
|
||||
match event {
|
||||
MidiMessage::NoteOn { key, .. } => {
|
||||
let note = key.as_int() as usize;
|
||||
let cell = target.get_mut(x, note).unwrap();
|
||||
cell.set_char('█');
|
||||
cell.set_style(style);
|
||||
notes_on[note] = true
|
||||
},
|
||||
MidiMessage::NoteOff { key, .. } => {
|
||||
notes_on[key.as_int() as usize] = false
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -122,12 +122,13 @@ impl<'a, T: HasPhraseList> From<&'a T> for PhraseListView<'a> {
|
|||
// TODO: Display phrases always in order of appearance
|
||||
render!(|self: PhraseListView<'a>|{
|
||||
let Self { title, focused, entered, phrases, index, mode } = self;
|
||||
let bg = if *focused {TuiTheme::g(32)} else {TuiTheme::null()};
|
||||
let border_bg = if *focused {TuiTheme::bg()} else {TuiTheme::null()};
|
||||
let border_color = if *entered {TuiTheme::bo1()} else {TuiTheme::bo2()};
|
||||
let title_color = if *focused {TuiTheme::ti1()} else {TuiTheme::ti2()};
|
||||
let upper_left = format!("{title}");
|
||||
let upper_right = format!("({})", phrases.len());
|
||||
Tui::bg(border_bg, lay!(move|add|{
|
||||
Tui::bg(bg, lay!(move|add|{
|
||||
//add(&Lozenge(Style::default().bg(border_bg).fg(border_color)))?;
|
||||
add(&Tui::inset_xy(0, 1, Tui::fill_xy(col!(move|add|match mode {
|
||||
Some(PhrasesMode::Import(_, ref browser)) => {
|
||||
|
|
@ -289,14 +290,14 @@ fn to_phrases_command (state: &PhraseListModel, input: &TuiInput) -> Option<Phra
|
|||
return None
|
||||
},
|
||||
key!(Char('a')) => Cmd::Phrase(Pool::Add(count, Phrase::new(
|
||||
String::from("(new)"), true, 4 * PPQ, None, Some(ItemColorTriplet::random())
|
||||
String::from("(new)"), true, 4 * PPQ, None, Some(ItemPalette::random())
|
||||
))),
|
||||
key!(Char('i')) => Cmd::Phrase(Pool::Add(index + 1, Phrase::new(
|
||||
String::from("(new)"), true, 4 * PPQ, None, Some(ItemColorTriplet::random())
|
||||
String::from("(new)"), true, 4 * PPQ, None, Some(ItemPalette::random())
|
||||
))),
|
||||
key!(Char('d')) => {
|
||||
let mut phrase = state.phrases()[index].read().unwrap().duplicate();
|
||||
phrase.color = ItemColorTriplet::random_near(phrase.color, 0.25);
|
||||
phrase.color = ItemPalette::random_near(phrase.color, 0.25);
|
||||
Cmd::Phrase(Pool::Add(index + 1, phrase))
|
||||
},
|
||||
_ => return None
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue