mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-06 11:46:41 +01:00
Compare commits
17 commits
3f1a2fee80
...
2858b01bd4
| Author | SHA1 | Date | |
|---|---|---|---|
| 2858b01bd4 | |||
| eb0547dc37 | |||
| 3e748fefa7 | |||
| f938ade839 | |||
| 4f575246ef | |||
| aeb1f7a9e0 | |||
| 29b2789be6 | |||
| f1f5ac63e1 | |||
| 62bfb0120b | |||
| 701ea3fc27 | |||
| ef6aa9ab07 | |||
| 5ed69edd02 | |||
| 4f3a50f2d6 | |||
| b0393184fa | |||
| d3d60d69c7 | |||
| 5ff6868a17 | |||
| c7e7c9f68c |
24 changed files with 925 additions and 1085 deletions
|
|
@ -3,12 +3,12 @@
|
|||
(info "A session grid.")
|
||||
|
||||
(keys
|
||||
(layer-if :is-editing "./keys_editor.edn")
|
||||
(layer-if :focus-message "./keys_message.edn")
|
||||
(layer-if :focus-device-add "./keys_device_add.edn")
|
||||
(layer-if :focus-browser "./keys_browser.edn")
|
||||
(layer-if :focus-pool-rename "./keys_rename.edn")
|
||||
(layer-if :focus-pool-length "./keys_length.edn")
|
||||
(layer-if :focus-editor "./keys_editor.edn")
|
||||
(layer-if :focus-clip "./keys_clip.edn")
|
||||
(layer-if :focus-track "./keys_track.edn")
|
||||
(layer-if :focus-scene "./keys_scene.edn")
|
||||
|
|
@ -17,14 +17,15 @@
|
|||
(layer "./keys_arranger.edn")
|
||||
(layer "./keys_global.edn"))
|
||||
|
||||
(view (bsp/a :view-dialog
|
||||
(bsp/s
|
||||
(fixed/y 8 (bsp/e
|
||||
(fixed/x 20 (fill/y (align/n (bsp/s :view-status-v
|
||||
(bsp/s :view-audio-ins-status :view-audio-outs-status)))))
|
||||
(fill/xy (align/n (bsp/s :view-arranger-track-names
|
||||
(bsp/s :view-arranger-track-outputs
|
||||
(bsp/s :view-arranger-track-devices :view-arranger-track-inputs)))))))
|
||||
(fill/xy (bsp/e
|
||||
(bsp/n (max/y 9 :view-editor-status) (fixed/x 20 (align/nw :view-arranger-scenes-names)))
|
||||
:view-arranger-scenes-clips)))))
|
||||
(view
|
||||
(bsp/a :view-dialog
|
||||
(bsp/w :view-meters-output
|
||||
(bsp/e :view-meters-input
|
||||
(bsp/n (fixed/y 2 :view-status-h2)
|
||||
(bsp/n (fill/x (align/w :view-tracks-inputs))
|
||||
(bsp/s (fill/x (align/w :view-tracks-devices))
|
||||
(bsp/s (fill/x (align/w :view-tracks-outputs))
|
||||
(bsp/s (fill/x (align/w :view-tracks-names))
|
||||
(fill/xy (either :is-editing
|
||||
(bsp/e (fixed/x 20 :view-scenes-names) :view-editor)
|
||||
:view-scenes)))))))))))
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
(@c color)
|
||||
(@q launch)
|
||||
(@t select :select-track-header)
|
||||
(@tab edit :clip-selected)
|
||||
(@shift-I input add)
|
||||
(@shift-O output add)
|
||||
(@shift-S scene add)
|
||||
(@shift-T track add)
|
||||
(@shift-D toggle-dialog :dialog-device)
|
||||
(@s select :select-scene-header)
|
||||
(@tab project edit)
|
||||
(@enter project edit)
|
||||
(@escape project home)
|
||||
(@shift-I project input-add)
|
||||
(@shift-O project output-add)
|
||||
(@shift-S project scene-add)
|
||||
(@shift-T project track-add)
|
||||
(@shift-D dialog :dialog-device)
|
||||
|
||||
(@up select :select-scene-prev)
|
||||
(@down select :select-scene-next)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
(@esc dialog :dialog-none)
|
||||
(@f1 dialog :dialog-help)
|
||||
(@f6 dialog :dialog-save)
|
||||
(@f8 dialog :dialog-options)
|
||||
(@f9 dialog :dialog-load)
|
||||
(@f10 dialog :dialog-quit)
|
||||
(@esc dialog :dialog-none)
|
||||
(@f1 dialog :dialog-help)
|
||||
(@f6 dialog :dialog-save)
|
||||
(@f8 dialog :dialog-options)
|
||||
(@f9 dialog :dialog-load)
|
||||
(@f10 dialog :dialog-quit)
|
||||
|
||||
(@u undo 1)
|
||||
(@r redo 1)
|
||||
|
|
|
|||
|
|
@ -18,20 +18,6 @@ handle!(TuiIn: |self: App, input|Ok(if let Some(command) = self.config.keys.comm
|
|||
}));
|
||||
|
||||
#[tengri_proc::command(App)] impl AppCommand {
|
||||
fn edit (app: &mut App) -> Perhaps<Self> {
|
||||
let selection = app.selection().clone();
|
||||
Ok(match selection {
|
||||
Selection::TrackClip { track, scene } => {
|
||||
let clip = &mut app.scenes_mut()[scene].clips[track];
|
||||
if clip.is_none() {
|
||||
*clip = Some(Default::default());
|
||||
}
|
||||
app.editor = clip.as_ref().map(|c|c.into());
|
||||
None
|
||||
}
|
||||
_ => None
|
||||
})
|
||||
}
|
||||
fn dialog (app: &mut App, dialog: Option<Dialog>) -> Perhaps<Self> {
|
||||
app.toggle_dialog(dialog);
|
||||
Ok(None)
|
||||
|
|
@ -62,18 +48,19 @@ handle!(TuiIn: |self: App, input|Ok(if let Some(command) = self.config.keys.comm
|
|||
//}
|
||||
fn select (app: &mut App, selection: Selection) -> Perhaps<Self> {
|
||||
*app.project.selection_mut() = selection;
|
||||
if let Some(ref mut editor) = app.editor {
|
||||
editor.set_clip(match app.project.selection() {
|
||||
Selection::TrackClip { track, scene } if let Some(Some(Some(clip))) = app
|
||||
.project
|
||||
.scenes.get(*scene)
|
||||
.map(|s|s.clips.get(*track))
|
||||
=>
|
||||
Some(clip),
|
||||
_ =>
|
||||
None
|
||||
});
|
||||
}
|
||||
//todo!
|
||||
//if let Some(ref mut editor) = app.editor_mut() {
|
||||
//editor.set_clip(match selection {
|
||||
//Selection::TrackClip { track, scene } if let Some(Some(Some(clip))) = app
|
||||
//.project
|
||||
//.scenes.get(scene)
|
||||
//.map(|s|s.clips.get(track))
|
||||
//=>
|
||||
//Some(clip),
|
||||
//_ =>
|
||||
//None
|
||||
//});
|
||||
//}
|
||||
Ok(None)
|
||||
//("select" [t: usize, s: usize] Some(match (t.expect("no track"), s.expect("no scene")) {
|
||||
//(0, 0) => Self::Select(Selection::Mix),
|
||||
|
|
@ -102,7 +89,7 @@ handle!(TuiIn: |self: App, input|Ok(if let Some(command) = self.config.keys.comm
|
|||
Ok(command.delegate(app, |command|Self::Message{command})?)
|
||||
}
|
||||
fn editor (app: &mut App, command: MidiEditCommand) -> Perhaps<Self> {
|
||||
Ok(if let Some(editor) = app.editor.as_mut() {
|
||||
Ok(if let Some(editor) = app.editor_mut() {
|
||||
let undo = command.clone().delegate(editor, |command|AppCommand::Editor{command})?;
|
||||
// update linked sampler after editor action
|
||||
app.project.sampler_mut().map(|sampler|match command {
|
||||
|
|
@ -117,18 +104,20 @@ handle!(TuiIn: |self: App, input|Ok(if let Some(command) = self.config.keys.comm
|
|||
}
|
||||
fn pool (app: &mut App, command: PoolCommand) -> Perhaps<Self> {
|
||||
let undo = command.clone().delegate(
|
||||
&mut app.project.pool,
|
||||
&mut app.pool,
|
||||
|command|AppCommand::Pool{command}
|
||||
)?;
|
||||
// update linked editor after pool action
|
||||
app.editor.as_mut().map(|editor|match command {
|
||||
match command {
|
||||
// autoselect: automatically load selected clip in editor
|
||||
PoolCommand::Select { .. } |
|
||||
// autocolor: update color in all places simultaneously
|
||||
PoolCommand::Clip { command: PoolClipCommand::SetColor { .. } } =>
|
||||
editor.set_clip(app.project.pool.clip().as_ref()),
|
||||
_ => {}
|
||||
});
|
||||
PoolCommand::Clip { command: PoolClipCommand::SetColor { .. } } => {
|
||||
let clip = app.pool.clip().clone();
|
||||
app.editor_mut().map(|editor|editor.set_clip(clip.as_ref()))
|
||||
},
|
||||
_ => None
|
||||
};
|
||||
Ok(undo)
|
||||
}
|
||||
}
|
||||
|
|
@ -147,7 +136,7 @@ impl<'state> Context<'state, MidiEditCommand> for App {
|
|||
|
||||
impl<'state> Context<'state, PoolCommand> for App {
|
||||
fn get <'source> (&'state self, iter: &mut TokenIter<'source>) -> Option<PoolCommand> {
|
||||
Context::get(&self.project.pool, iter)
|
||||
Context::get(&self.pool, iter)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ audio!(
|
|||
let t0 = self.perf.get_t0();
|
||||
self.clock().update_from_scope(scope).unwrap();
|
||||
let midi_in = self.project.midi_input_collect(scope);
|
||||
if let Some(editor) = &self.editor {
|
||||
if let Some(editor) = &self.editor() {
|
||||
let mut pitch: Option<u7> = None;
|
||||
for port in midi_in.iter() {
|
||||
for event in port.iter() {
|
||||
|
|
|
|||
|
|
@ -1,199 +0,0 @@
|
|||
use crate::*;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Configuration
|
||||
#[derive(Default, Debug)]
|
||||
pub struct Configuration {
|
||||
/// Path of configuration entrypoint
|
||||
pub path: PathBuf,
|
||||
/// Name of configuration
|
||||
pub name: Option<Arc<str>>,
|
||||
/// Description of configuration
|
||||
pub info: Option<Arc<str>>,
|
||||
/// View definition
|
||||
pub view: TokenIter<'static>,
|
||||
// Input keymap
|
||||
pub keys: InputMap<'static, App, AppCommand, TuiIn, TokenIter<'static>>
|
||||
}
|
||||
|
||||
impl Configuration {
|
||||
|
||||
pub fn new (path: &impl AsRef<Path>, _watch: bool) -> Usually<Self> {
|
||||
let text = read_and_leak(path.as_ref())?;
|
||||
let [name, info, view, keys] = Self::parse(TokenIter::from(text))?;
|
||||
Ok(Self {
|
||||
path: path.as_ref().into(),
|
||||
info: info.map(Self::parse_info).flatten(),
|
||||
name: name.map(Self::parse_name).flatten(),
|
||||
view: Self::parse_view(view)?,
|
||||
keys: Self::parse_keys(&path, keys)?,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse (iter: TokenIter) -> Usually<[Option<TokenIter>;4]> {
|
||||
let mut name: Option<TokenIter> = None;
|
||||
let mut info: Option<TokenIter> = None;
|
||||
let mut view: Option<TokenIter> = None;
|
||||
let mut keys: Option<TokenIter> = None;
|
||||
for token in iter {
|
||||
match token.value {
|
||||
Value::Exp(_, mut exp) => {
|
||||
let next = exp.next();
|
||||
match next {
|
||||
Some(Token { value: Value::Key(sym), .. }) => match sym {
|
||||
"name" => name = Some(exp),
|
||||
"info" => info = Some(exp),
|
||||
"keys" => keys = Some(exp),
|
||||
"view" => view = Some(exp),
|
||||
_ => return Err(
|
||||
format!("(e3) unexpected symbol {sym:?}").into()
|
||||
)
|
||||
},
|
||||
_ => return Err(
|
||||
format!("(e2) unexpected exp {:?}", next.map(|x|x.value)).into()
|
||||
)
|
||||
}
|
||||
},
|
||||
t => return Err(
|
||||
format!("(e1) unexpected token {token:?}").into()
|
||||
)
|
||||
};
|
||||
}
|
||||
Ok([name, info, view, keys])
|
||||
}
|
||||
|
||||
fn parse_info (mut iter: TokenIter) -> Option<Arc<str>> {
|
||||
iter.next().and_then(|x|if let Value::Str(x) = x.value {
|
||||
Some(x.into())
|
||||
} else {
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_name (mut iter: TokenIter) -> Option<Arc<str>> {
|
||||
iter.next().and_then(|x|if let Value::Str(x) = x.value {
|
||||
Some(x.into())
|
||||
} else {
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_view (iter: Option<TokenIter>) -> Usually<TokenIter> {
|
||||
if let Some(view) = iter {
|
||||
Ok(view)
|
||||
} else {
|
||||
Err(format!("missing view definition").into())
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_keys (base: &impl AsRef<Path>, iter: Option<TokenIter<'static>>)
|
||||
-> Usually<InputMap<'static, App, AppCommand, TuiIn, TokenIter<'static>>>
|
||||
{
|
||||
if iter.is_none() {
|
||||
return Err(format!("missing keys definition").into())
|
||||
}
|
||||
let mut keys = iter.unwrap();
|
||||
let mut map = InputMap::default();
|
||||
while let Some(token) = keys.next() {
|
||||
if let Value::Exp(_, mut exp) = token.value {
|
||||
let next = exp.next();
|
||||
if let Some(Token { value: Value::Key(sym), .. }) = next {
|
||||
match sym {
|
||||
"layer" => {
|
||||
let next = exp.next();
|
||||
if let Some(Token { value: Value::Str(path), .. }) = next {
|
||||
let path = base.as_ref().parent().unwrap().join(unquote(path));
|
||||
if !std::fs::exists(&path)? {
|
||||
return Err(format!("(e5) not found: {path:?}").into())
|
||||
}
|
||||
map.add_layer(read_and_leak(path)?.into());
|
||||
} else {
|
||||
return Err(format!("(e4) unexpected non-string {next:?}").into())
|
||||
}
|
||||
},
|
||||
|
||||
"layer-if" => {
|
||||
let mut cond = None;
|
||||
|
||||
let next = exp.next();
|
||||
if let Some(Token { value: Value::Sym(sym), .. }) = next {
|
||||
cond = Some(leak(sym));
|
||||
} else {
|
||||
return Err(format!("(e4) unexpected non-symbol {next:?}").into())
|
||||
};
|
||||
|
||||
if let Some(Token { value: Value::Str(path), .. }) = exp.peek() {
|
||||
let path = base.as_ref().parent().unwrap().join(unquote(path));
|
||||
if !std::fs::exists(&path)? {
|
||||
return Err(format!("(e5) not found: {path:?}").into())
|
||||
}
|
||||
print!("{path:?}...");
|
||||
let keys = read_and_leak(path)?.into();
|
||||
let cond = cond.unwrap();
|
||||
print!("{exp:?}...");
|
||||
println!("ok");
|
||||
map.add_layer_if(
|
||||
Box::new(move |state|{
|
||||
let mut exp = exp.clone();
|
||||
Context::get(state, &mut exp).unwrap_or(false)
|
||||
}),
|
||||
keys
|
||||
);
|
||||
} else {
|
||||
return Err(format!("(e4) unexpected non-symbol {next:?}").into())
|
||||
}
|
||||
},
|
||||
|
||||
_ => return Err(format!("(e3) unexpected symbol {sym:?}").into())
|
||||
}
|
||||
} else {
|
||||
return Err(format!("(e2) unexpected exp {:?}", next.map(|x|x.value)).into())
|
||||
}
|
||||
} else {
|
||||
return Err(format!("(e1) unexpected token {token:?}").into())
|
||||
}
|
||||
}
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fn read_and_leak (path: impl AsRef<Path>) -> Usually<&'static str> {
|
||||
Ok(leak(String::from_utf8(std::fs::read(path.as_ref())?)?))
|
||||
}
|
||||
|
||||
fn leak (x: impl AsRef<str>) -> &'static str {
|
||||
Box::leak(x.as_ref().into())
|
||||
}
|
||||
|
||||
fn unquote (x: &str) -> &str {
|
||||
let mut chars = x.chars();
|
||||
chars.next();
|
||||
//chars.next_back();
|
||||
chars.as_str()
|
||||
}
|
||||
|
||||
macro_rules! default_config { ($path:literal) => { ($path, include_str!($path)) }; }
|
||||
pub const DEFAULT_CONFIGS: &'static [(&'static str, &'static str)] = &[
|
||||
default_config!("../../../config/config_arranger.edn"),
|
||||
default_config!("../../../config/config_groovebox.edn"),
|
||||
default_config!("../../../config/config_sampler.edn"),
|
||||
default_config!("../../../config/config_sequencer.edn"),
|
||||
default_config!("../../../config/config_transport.edn"),
|
||||
|
||||
default_config!("../../../config/keys_arranger.edn"),
|
||||
default_config!("../../../config/keys_clip.edn"),
|
||||
default_config!("../../../config/keys_clock.edn"),
|
||||
default_config!("../../../config/keys_editor.edn"),
|
||||
default_config!("../../../config/keys_global.edn"),
|
||||
default_config!("../../../config/keys_groovebox.edn"),
|
||||
default_config!("../../../config/keys_length.edn"),
|
||||
default_config!("../../../config/keys_mix.edn"),
|
||||
default_config!("../../../config/keys_pool.edn"),
|
||||
default_config!("../../../config/keys_pool_file.edn"),
|
||||
default_config!("../../../config/keys_rename.edn"),
|
||||
default_config!("../../../config/keys_sampler.edn"),
|
||||
default_config!("../../../config/keys_scene.edn"),
|
||||
default_config!("../../../config/keys_sequencer.edn"),
|
||||
default_config!("../../../config/keys_track.edn"),
|
||||
];
|
||||
|
|
@ -36,7 +36,6 @@ pub(crate) use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering::Relaxed};
|
|||
|
||||
mod api; pub use self::api::*;
|
||||
mod audio; pub use self::audio::*;
|
||||
mod config; pub use self::config::*;
|
||||
mod model; pub use self::model::*;
|
||||
mod view; pub use self::view::*;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
use crate::*;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct App {
|
||||
|
|
@ -20,23 +21,23 @@ pub struct App {
|
|||
pub history: Vec<AppCommand>,
|
||||
// Dialog overlay
|
||||
pub dialog: Option<Dialog>,
|
||||
/// Contains the currently edited MIDI clip
|
||||
pub editor: Option<MidiEditor>,
|
||||
// Cache of formatted strings
|
||||
pub view_cache: Arc<RwLock<ViewCache>>,
|
||||
/// Base color.
|
||||
pub color: ItemTheme,
|
||||
}
|
||||
|
||||
has!(Jack: |self: App|self.jack);
|
||||
has!(Pool: |self: App|self.pool);
|
||||
has!(Clock: |self: App|self.project.clock);
|
||||
has!(Selection: |self: App|self.project.selection);
|
||||
has!(Vec<JackMidiIn>: |self: App|self.project.midi_ins);
|
||||
has!(Vec<JackMidiOut>: |self: App|self.project.midi_outs);
|
||||
has!(Vec<Scene>: |self: App|self.project.scenes);
|
||||
has!(Vec<Track>: |self: App|self.project.tracks);
|
||||
has!(Measure<TuiOut>: |self: App|self.size);
|
||||
has!(Jack: |self: App|self.jack);
|
||||
has!(Pool: |self: App|self.pool);
|
||||
has!(Option<Dialog>: |self: App|self.dialog);
|
||||
has!(Clock: |self: App|self.project.clock);
|
||||
has!(Option<MidiEditor>: |self: App|self.project.editor);
|
||||
has!(Selection: |self: App|self.project.selection);
|
||||
has!(Vec<JackMidiIn>: |self: App|self.project.midi_ins);
|
||||
has!(Vec<JackMidiOut>: |self: App|self.project.midi_outs);
|
||||
has!(Vec<Scene>: |self: App|self.project.scenes);
|
||||
has!(Vec<Track>: |self: App|self.project.tracks);
|
||||
has!(Measure<TuiOut>: |self: App|self.size);
|
||||
maybe_has!(Track: |self: App|
|
||||
{ MaybeHas::<Track>::get(&self.project) };
|
||||
{ MaybeHas::<Track>::get_mut(&mut self.project) });
|
||||
|
|
@ -49,28 +50,34 @@ maybe_has!(Scene: |self: App|
|
|||
impl HasSceneScroll for App {
|
||||
fn scene_scroll (&self) -> usize { self.project.scene_scroll() }
|
||||
}
|
||||
has_clips!(|self: App|self.project.pool.clips);
|
||||
has_editor!(|self: App|{
|
||||
editor = self.editor;
|
||||
editor_w = {
|
||||
let size = self.size.w();
|
||||
let editor = self.editor.as_ref().expect("missing editor");
|
||||
let time_len = editor.time_len().get();
|
||||
let time_zoom = editor.time_zoom().get().max(1);
|
||||
(5 + (time_len / time_zoom)).min(size.saturating_sub(20)).max(16)
|
||||
};
|
||||
editor_h = 15;
|
||||
is_editing = self.editor.is_some();
|
||||
});
|
||||
has_clips!(|self: App|self.pool.clips);
|
||||
impl HasClipsSize for App {
|
||||
fn clips_size (&self) -> &Measure<TuiOut> { &self.project.inner_size }
|
||||
}
|
||||
//has_editor!(|self: App|{
|
||||
//editor = self.editor;
|
||||
//editor_w = {
|
||||
//let size = self.size.w();
|
||||
//let editor = self.editor.as_ref().expect("missing editor");
|
||||
//let time_len = editor.time_len().get();
|
||||
//let time_zoom = editor.time_zoom().get().max(1);
|
||||
//(5 + (time_len / time_zoom)).min(size.saturating_sub(20)).max(16)
|
||||
//};
|
||||
//editor_h = 15;
|
||||
//is_editing = self.editor.is_some();
|
||||
//});
|
||||
|
||||
impl App {
|
||||
pub fn update_clock (&self) {
|
||||
ViewCache::update_clock(&self.view_cache, self.clock(), self.size.w() > 80)
|
||||
}
|
||||
pub fn toggle_dialog (&mut self, mut dialog: Option<Dialog>) -> Option<Dialog> {
|
||||
std::mem::swap(&mut self.dialog, &mut dialog);
|
||||
dialog
|
||||
}
|
||||
pub fn toggle_editor (&mut self, value: Option<bool>) {
|
||||
//FIXME: self.editing.store(value.unwrap_or_else(||!self.is_editing()), Relaxed);
|
||||
let value = value.unwrap_or_else(||!self.editor.is_some());
|
||||
let value = value.unwrap_or_else(||!self.editor().is_some());
|
||||
if value {
|
||||
self.clip_auto_create();
|
||||
} else {
|
||||
|
|
@ -86,12 +93,6 @@ impl App {
|
|||
pub(crate) fn device_pick (&mut self, index: usize) {
|
||||
self.dialog = Some(Dialog::Device(index));
|
||||
}
|
||||
pub(crate) fn device_kinds (&self) -> &'static [&'static str] {
|
||||
&[
|
||||
"Sampler",
|
||||
"Plugin (LV2)",
|
||||
]
|
||||
}
|
||||
pub(crate) fn device_add (&mut self, index: usize) -> Usually<()> {
|
||||
match index {
|
||||
0 => self.device_add_sampler(),
|
||||
|
|
@ -130,11 +131,11 @@ impl App {
|
|||
&& slot.is_none()
|
||||
&& let Some(track) = self.project.tracks.get_mut(track)
|
||||
{
|
||||
let (index, mut clip) = self.project.pool.add_new_clip();
|
||||
let (index, mut clip) = self.pool.add_new_clip();
|
||||
// autocolor: new clip colors from scene and track color
|
||||
let color = track.color.base.mix(scene.color.base, 0.5);
|
||||
clip.write().unwrap().color = ItemColor::random_near(color, 0.2).into();
|
||||
if let Some(ref mut editor) = self.editor {
|
||||
if let Some(ref mut editor) = &mut self.project.editor {
|
||||
editor.set_clip(Some(&clip));
|
||||
}
|
||||
*slot = Some(clip.clone());
|
||||
|
|
@ -155,41 +156,12 @@ impl App {
|
|||
std::mem::swap(&mut swapped, slot);
|
||||
}
|
||||
if let Some(clip) = swapped {
|
||||
self.project.pool.delete_clip(&clip.read().unwrap());
|
||||
self.pool.delete_clip(&clip.read().unwrap());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Various possible dialog overlays
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Dialog {
|
||||
Help(usize),
|
||||
Menu(usize),
|
||||
Device(usize),
|
||||
Message(Message),
|
||||
Browser(BrowserTarget, Browser),
|
||||
Options,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum BrowserTarget {
|
||||
SaveProject,
|
||||
LoadProject,
|
||||
ImportSample(Arc<RwLock<Option<Sample>>>),
|
||||
ExportSample(Arc<RwLock<Option<Sample>>>),
|
||||
ImportClip(Arc<RwLock<Option<MidiClip>>>),
|
||||
ExportClip(Arc<RwLock<Option<MidiClip>>>),
|
||||
}
|
||||
|
||||
/// Various possible messages
|
||||
#[derive(PartialEq, Clone, Copy, Debug)]
|
||||
pub enum Message {
|
||||
FailedToAddDevice,
|
||||
}
|
||||
|
||||
content!(TuiOut: |self: Message| match self { Self::FailedToAddDevice => "Failed to add device." });
|
||||
|
||||
#[tengri_proc::expose]
|
||||
impl App {
|
||||
fn _todo_isize_stub (&self) -> isize {
|
||||
|
|
@ -199,16 +171,16 @@ impl App {
|
|||
todo!()
|
||||
}
|
||||
fn w_sidebar (&self) -> u16 {
|
||||
self.project.w_sidebar(self.editor.is_some())
|
||||
self.project.w_sidebar(self.editor().is_some())
|
||||
}
|
||||
fn h_sample_detail (&self) -> u16 {
|
||||
6.max(self.height() as u16 * 3 / 9)
|
||||
}
|
||||
fn focus_editor (&self) -> bool {
|
||||
self.is_editing()
|
||||
self.project.editor.is_some()
|
||||
}
|
||||
fn is_editing (&self) -> bool {
|
||||
HasEditor::is_editing(self)
|
||||
self.project.editor.is_some()
|
||||
}
|
||||
fn focus_message (&self) -> bool {
|
||||
matches!(self.dialog, Some(Dialog::Message(..)))
|
||||
|
|
@ -232,16 +204,16 @@ impl App {
|
|||
!self.is_editing() && self.selection().is_mix()
|
||||
}
|
||||
fn focus_pool_import (&self) -> bool {
|
||||
matches!(self.project.pool.mode, Some(PoolMode::Import(..)))
|
||||
matches!(self.pool.mode, Some(PoolMode::Import(..)))
|
||||
}
|
||||
fn focus_pool_export (&self) -> bool {
|
||||
matches!(self.project.pool.mode, Some(PoolMode::Export(..)))
|
||||
matches!(self.pool.mode, Some(PoolMode::Export(..)))
|
||||
}
|
||||
fn focus_pool_rename (&self) -> bool {
|
||||
matches!(self.project.pool.mode, Some(PoolMode::Rename(..)))
|
||||
matches!(self.pool.mode, Some(PoolMode::Rename(..)))
|
||||
}
|
||||
fn focus_pool_length (&self) -> bool {
|
||||
matches!(self.project.pool.mode, Some(PoolMode::Length(..)))
|
||||
matches!(self.pool.mode, Some(PoolMode::Length(..)))
|
||||
}
|
||||
fn dialog_none (&self) -> Option<Dialog> {
|
||||
None
|
||||
|
|
@ -327,16 +299,214 @@ impl App {
|
|||
}
|
||||
fn device_kind_prev (&self) -> usize {
|
||||
if let Some(Dialog::Device(index)) = self.dialog {
|
||||
index.overflowing_sub(1).0.min(self.device_kinds().len().saturating_sub(1))
|
||||
index.overflowing_sub(1).0.min(device_kinds().len().saturating_sub(1))
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
fn device_kind_next (&self) -> usize {
|
||||
if let Some(Dialog::Device(index)) = self.dialog {
|
||||
(index + 1) % self.device_kinds().len()
|
||||
(index + 1) % device_kinds().len()
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration
|
||||
#[derive(Default, Debug)]
|
||||
pub struct Configuration {
|
||||
/// Path of configuration entrypoint
|
||||
pub path: PathBuf,
|
||||
/// Name of configuration
|
||||
pub name: Option<Arc<str>>,
|
||||
/// Description of configuration
|
||||
pub info: Option<Arc<str>>,
|
||||
/// View definition
|
||||
pub view: TokenIter<'static>,
|
||||
// Input keymap
|
||||
pub keys: InputMap<'static, App, AppCommand, TuiIn, TokenIter<'static>>,
|
||||
}
|
||||
|
||||
impl Configuration {
|
||||
|
||||
pub fn new (path: &impl AsRef<Path>, _watch: bool) -> Usually<Self> {
|
||||
let text = read_and_leak(path.as_ref())?;
|
||||
let [name, info, view, keys] = Self::parse(TokenIter::from(text))?;
|
||||
Ok(Self {
|
||||
path: path.as_ref().into(),
|
||||
info: info.map(Self::parse_info).flatten(),
|
||||
name: name.map(Self::parse_name).flatten(),
|
||||
view: Self::parse_view(view)?,
|
||||
keys: Self::parse_keys(&path, keys)?,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse (iter: TokenIter) -> Usually<[Option<TokenIter>;4]> {
|
||||
let mut name: Option<TokenIter> = None;
|
||||
let mut info: Option<TokenIter> = None;
|
||||
let mut view: Option<TokenIter> = None;
|
||||
let mut keys: Option<TokenIter> = None;
|
||||
for token in iter {
|
||||
match token.value {
|
||||
Value::Exp(_, mut exp) => {
|
||||
let next = exp.next();
|
||||
match next {
|
||||
Some(Token { value: Value::Key(sym), .. }) => match sym {
|
||||
"name" => name = Some(exp),
|
||||
"info" => info = Some(exp),
|
||||
"keys" => keys = Some(exp),
|
||||
"view" => view = Some(exp),
|
||||
_ => return Err(
|
||||
format!("(e3) unexpected symbol {sym:?}").into()
|
||||
)
|
||||
},
|
||||
_ => return Err(
|
||||
format!("(e2) unexpected exp {:?}", next.map(|x|x.value)).into()
|
||||
)
|
||||
}
|
||||
},
|
||||
t => return Err(
|
||||
format!("(e1) unexpected token {token:?}").into()
|
||||
)
|
||||
};
|
||||
}
|
||||
Ok([name, info, view, keys])
|
||||
}
|
||||
|
||||
fn parse_info (mut iter: TokenIter) -> Option<Arc<str>> {
|
||||
iter.next().and_then(|x|if let Value::Str(x) = x.value {
|
||||
Some(x.into())
|
||||
} else {
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_name (mut iter: TokenIter) -> Option<Arc<str>> {
|
||||
iter.next().and_then(|x|if let Value::Str(x) = x.value {
|
||||
Some(x.into())
|
||||
} else {
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_view (iter: Option<TokenIter>) -> Usually<TokenIter> {
|
||||
if let Some(view) = iter {
|
||||
Ok(view)
|
||||
} else {
|
||||
Err(format!("missing view definition").into())
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_keys (base: &impl AsRef<Path>, iter: Option<TokenIter<'static>>)
|
||||
-> Usually<InputMap<'static, App, AppCommand, TuiIn, TokenIter<'static>>>
|
||||
{
|
||||
if iter.is_none() {
|
||||
return Err(format!("missing keys definition").into())
|
||||
}
|
||||
let mut keys = iter.unwrap();
|
||||
let mut map = InputMap::default();
|
||||
while let Some(token) = keys.next() {
|
||||
if let Value::Exp(_, mut exp) = token.value {
|
||||
let next = exp.next();
|
||||
if let Some(Token { value: Value::Key(sym), .. }) = next {
|
||||
match sym {
|
||||
"layer" => {
|
||||
if let Some(Token { value: Value::Str(path), .. }) = exp.peek() {
|
||||
let path = base.as_ref().parent().unwrap().join(unquote(path));
|
||||
if !std::fs::exists(&path)? {
|
||||
return Err(format!("(e5) not found: {path:?}").into())
|
||||
}
|
||||
map.add_layer(read_and_leak(path)?.into());
|
||||
print!("layer:\n path: {:?}...", exp.0.0.trim());
|
||||
println!("ok");
|
||||
} else {
|
||||
return Err(format!("(e4) unexpected non-string {next:?}").into())
|
||||
}
|
||||
},
|
||||
|
||||
"layer-if" => {
|
||||
let mut cond = None;
|
||||
|
||||
let next = exp.next();
|
||||
if let Some(Token { value: Value::Sym(sym), .. }) = next {
|
||||
cond = Some(leak(sym));
|
||||
} else {
|
||||
return Err(format!("(e4) unexpected non-symbol {next:?}").into())
|
||||
};
|
||||
|
||||
if let Some(Token { value: Value::Str(path), .. }) = exp.peek() {
|
||||
let path = base.as_ref().parent().unwrap().join(unquote(path));
|
||||
if !std::fs::exists(&path)? {
|
||||
return Err(format!("(e5) not found: {path:?}").into())
|
||||
}
|
||||
print!("layer-if:\n cond: {}\n path: {path:?}...",
|
||||
cond.unwrap_or_default());
|
||||
let keys = read_and_leak(path)?.into();
|
||||
let cond = cond.unwrap();
|
||||
println!("ok");
|
||||
map.add_layer_if(
|
||||
Box::new(move |state|{
|
||||
let mut exp = exp.clone();
|
||||
Context::get(state, &mut exp).unwrap_or(false)
|
||||
}),
|
||||
keys
|
||||
);
|
||||
} else {
|
||||
return Err(format!("(e4) unexpected non-symbol {next:?}").into())
|
||||
}
|
||||
},
|
||||
|
||||
_ => return Err(format!("(e3) unexpected symbol {sym:?}").into())
|
||||
}
|
||||
} else {
|
||||
return Err(format!("(e2) unexpected exp {:?}", next.map(|x|x.value)).into())
|
||||
}
|
||||
} else {
|
||||
return Err(format!("(e1) unexpected token {token:?}").into())
|
||||
}
|
||||
}
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fn read_and_leak (path: impl AsRef<Path>) -> Usually<&'static str> {
|
||||
Ok(leak(String::from_utf8(std::fs::read(path.as_ref())?)?))
|
||||
}
|
||||
|
||||
fn leak (x: impl AsRef<str>) -> &'static str {
|
||||
Box::leak(x.as_ref().into())
|
||||
}
|
||||
|
||||
fn unquote (x: &str) -> &str {
|
||||
let mut chars = x.chars();
|
||||
chars.next();
|
||||
//chars.next_back();
|
||||
chars.as_str()
|
||||
}
|
||||
|
||||
macro_rules! default_config { ($path:literal) => { ($path, include_str!($path)) }; }
|
||||
pub const DEFAULT_CONFIGS: &'static [(&'static str, &'static str)] = &[
|
||||
default_config!("../../../config/config_arranger.edn"),
|
||||
default_config!("../../../config/config_groovebox.edn"),
|
||||
default_config!("../../../config/config_sampler.edn"),
|
||||
default_config!("../../../config/config_sequencer.edn"),
|
||||
default_config!("../../../config/config_transport.edn"),
|
||||
|
||||
default_config!("../../../config/keys_arranger.edn"),
|
||||
default_config!("../../../config/keys_clip.edn"),
|
||||
default_config!("../../../config/keys_clock.edn"),
|
||||
default_config!("../../../config/keys_editor.edn"),
|
||||
default_config!("../../../config/keys_global.edn"),
|
||||
default_config!("../../../config/keys_groovebox.edn"),
|
||||
default_config!("../../../config/keys_length.edn"),
|
||||
default_config!("../../../config/keys_mix.edn"),
|
||||
default_config!("../../../config/keys_pool.edn"),
|
||||
default_config!("../../../config/keys_pool_file.edn"),
|
||||
default_config!("../../../config/keys_rename.edn"),
|
||||
default_config!("../../../config/keys_sampler.edn"),
|
||||
default_config!("../../../config/keys_scene.edn"),
|
||||
default_config!("../../../config/keys_sequencer.edn"),
|
||||
default_config!("../../../config/keys_track.edn"),
|
||||
];
|
||||
|
|
|
|||
|
|
@ -2,24 +2,55 @@ use crate::*;
|
|||
pub(crate) use std::fmt::Write;
|
||||
pub(crate) use ::tengri::tui::ratatui::prelude::Position;
|
||||
|
||||
impl App {
|
||||
|
||||
pub fn update_clock (&self) {
|
||||
ViewCache::update_clock(&self.view_cache, self.clock(), self.size.w() > 80)
|
||||
}
|
||||
}
|
||||
|
||||
#[tengri_proc::view(TuiOut)]
|
||||
impl App {
|
||||
pub fn view_nil (&self) -> impl Content<TuiOut> + use<'_> {
|
||||
"nil"
|
||||
}
|
||||
pub fn view_status_h2 (&self) -> impl Content<TuiOut> + use<'_> {
|
||||
self.update_clock();
|
||||
let theme = self.color;
|
||||
let playing = self.clock().is_rolling();
|
||||
Fixed::y(2, Stack::east(move|add: &mut dyn FnMut(&dyn Render<TuiOut>)|{
|
||||
add(&Fixed::x(5, Tui::bg(if playing { Rgb(0, 128, 0) } else { Rgb(128, 64, 0) },
|
||||
Either::new(false, // TODO
|
||||
Thunk::new(move||Fixed::x(9, Either::new(playing,
|
||||
Tui::fg(Rgb(0, 255, 0), " PLAYING "),
|
||||
Tui::fg(Rgb(255, 128, 0), " STOPPED ")))
|
||||
),
|
||||
Thunk::new(move||Fixed::x(5, Either::new(playing,
|
||||
Tui::fg(Rgb(0, 255, 0), Bsp::s(" 🭍🭑🬽 ", " 🭞🭜🭘 ",)),
|
||||
Tui::fg(Rgb(255, 128, 0), Bsp::s(" ▗▄▖ ", " ▝▀▘ ",))))
|
||||
)
|
||||
)
|
||||
)));
|
||||
{
|
||||
let cache = self.view_cache.read().unwrap();
|
||||
add(&Fixed::x(16, Align::w(Bsp::s(
|
||||
FieldH(theme, "Beat", cache.beat.view.clone()),
|
||||
FieldH(theme, "Time", cache.time.view.clone()),
|
||||
))));
|
||||
add(&Fixed::x(16, Align::w(Bsp::s(
|
||||
Fill::x(Align::w(FieldH(theme, "BPM", cache.bpm.view.clone()))),
|
||||
Fill::x(Align::w(FieldH(theme, "SR ", cache.sr.view.clone()))),
|
||||
))));
|
||||
add(&Fixed::x(16, Align::w(Bsp::s(
|
||||
Fill::x(Align::w(FieldH(theme, "Buf", cache.buf.view.clone()))),
|
||||
Fill::x(Align::w(FieldH(theme, "Lat", cache.lat.view.clone()))),
|
||||
))));
|
||||
add(&FieldV(theme, "Selection", Fill::x(Align::w(self.selection().describe(
|
||||
self.tracks(),
|
||||
self.scenes()
|
||||
)))));
|
||||
}
|
||||
}))
|
||||
}
|
||||
pub fn view_status_v (&self) -> impl Content<TuiOut> + use<'_> {
|
||||
self.update_clock();
|
||||
let cache = self.view_cache.read().unwrap();
|
||||
let theme = self.color;
|
||||
let playing = self.clock().is_rolling();
|
||||
Tui::bg(theme.darkest.rgb, Fixed::xy(20, 6, Outer(true, Style::default().fg(Tui::g(96))).enclose(
|
||||
Tui::bg(theme.darker.rgb, Fixed::xy(20, 5, Outer(true, Style::default().fg(Tui::g(96))).enclose(
|
||||
col!(
|
||||
Fill::x(Align::w(Bsp::e(
|
||||
Align::w(Tui::bg(if playing { Rgb(0, 128, 0) } else { Rgb(128, 64, 0) },
|
||||
|
|
@ -41,8 +72,7 @@ impl App {
|
|||
))),
|
||||
Fill::x(Align::w(FieldH(theme, "BPM", cache.bpm.view.clone()))),
|
||||
Fill::x(Align::w(FieldH(theme, "SR ", cache.sr.view.clone()))),
|
||||
Fill::x(Align::w(FieldH(theme, "Buf", cache.buf.view.clone()))),
|
||||
Fill::x(Align::w(FieldH(theme, "Lat", cache.lat.view.clone()))),
|
||||
Fill::x(Align::w(FieldH(theme, "Buf", Bsp::e(cache.buf.view.clone(), Bsp::e(" = ", cache.lat.view.clone()))))),
|
||||
))))
|
||||
}
|
||||
pub fn view_status (&self) -> impl Content<TuiOut> + use<'_> {
|
||||
|
|
@ -58,7 +88,11 @@ impl App {
|
|||
cache.bpm.view.clone(), cache.beat.view.clone(), cache.time.view.clone())
|
||||
}
|
||||
pub fn view_editor (&self) -> impl Content<TuiOut> + use<'_> {
|
||||
self.editor()
|
||||
let bg = self.editor()
|
||||
.and_then(|editor|editor.clip().clone())
|
||||
.map(|clip|clip.read().unwrap().color.darker)
|
||||
.unwrap_or(self.color.darker);
|
||||
Fill::xy(Tui::bg(bg.rgb, self.editor()))
|
||||
}
|
||||
pub fn view_editor_status (&self) -> impl Content<TuiOut> + use<'_> {
|
||||
self.editor().map(|e|Fixed::x(20, Outer(true, Style::default().fg(Tui::g(96))).enclose(
|
||||
|
|
@ -76,48 +110,35 @@ impl App {
|
|||
pub fn view_audio_outs_status (&self) -> impl Content<TuiOut> + use<'_> {
|
||||
self.project.view_audio_outs_status(self.color)
|
||||
}
|
||||
pub fn view_arranger (&self) -> impl Content<TuiOut> + use<'_> {
|
||||
&self.project
|
||||
pub fn view_scenes (&self) -> impl Content<TuiOut> + use<'_> {
|
||||
Bsp::e(
|
||||
Fixed::x(20, Align::nw(self.project.view_scenes_names())),
|
||||
self.project.view_scenes_clips(),
|
||||
)
|
||||
}
|
||||
pub fn view_arranger_scenes_names (&self) -> impl Content<TuiOut> + use<'_> {
|
||||
pub fn view_scenes_names (&self) -> impl Content<TuiOut> + use<'_> {
|
||||
self.project.view_scenes_names()
|
||||
}
|
||||
pub fn view_arranger_scenes_clips (&self) -> impl Content<TuiOut> + use<'_> {
|
||||
self.project.view_scenes_clips(&self.editor)
|
||||
pub fn view_scenes_clips (&self) -> impl Content<TuiOut> + use<'_> {
|
||||
self.project.view_scenes_clips()
|
||||
}
|
||||
pub fn view_arranger_track_names (&self) -> impl Content<TuiOut> + use<'_> {
|
||||
self.project.view_track_names(self.color)
|
||||
pub fn view_tracks_inputs <'a> (&'a self) -> impl Content<TuiOut> + use<'a> {
|
||||
Fixed::y(1 + self.project.midi_ins.len() as u16, self.project.view_inputs(self.color))
|
||||
}
|
||||
pub fn view_arranger_track_outputs <'a> (&'a self) -> impl Content<TuiOut> + use<'a> {
|
||||
self.project.view_track_outputs(self.color)
|
||||
pub fn view_tracks_outputs <'a> (&'a self) -> impl Content<TuiOut> + use<'a> {
|
||||
Fixed::y(1 + self.project.midi_outs.len() as u16, self.project.view_outputs(self.color))
|
||||
}
|
||||
pub fn view_arranger_track_inputs <'a> (&'a self) -> impl Content<TuiOut> + use<'a> {
|
||||
self.project.view_track_inputs(self.color)
|
||||
pub fn view_tracks_devices <'a> (&'a self) -> impl Content<TuiOut> + use<'a> {
|
||||
Fixed::y(3, self.project.view_track_devices(self.color))
|
||||
}
|
||||
pub fn view_arranger_track_devices <'a> (&'a self) -> impl Content<TuiOut> + use<'a> {
|
||||
self.project.view_track_devices(self.color)
|
||||
}
|
||||
pub fn view_arranger_track_scenes <'a> (&'a self) -> impl Content<TuiOut> + use<'a> {
|
||||
let mut max_devices = 0u16;
|
||||
for track in self.project.tracks.iter() {
|
||||
max_devices = max_devices.max(track.devices.len() as u16);
|
||||
}
|
||||
Bsp::w(
|
||||
Fixed::x(20, Tui::bg(self.color.darkest.rgb,
|
||||
col!(Tui::bold(true, "Devices"), "[d] Select", "[D] Add"))),
|
||||
Fixed::y(max_devices + 1, Tui::bg(self.color.darker.rgb, Align::w(Fill::x(Map::new(
|
||||
||self.project.tracks_with_sizes(&self.project.selection, None)
|
||||
.skip(self.project.track_scroll),
|
||||
move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|
|
||||
Push::x(x2 as u16, Fixed::xy(track.width as u16, max_devices + 1,
|
||||
Align::nw(Map::south(1, ||track.devices.iter(),
|
||||
|device, index|format!("{index}: {}", device.name())))))))))))
|
||||
pub fn view_tracks_names <'a> (&'a self) -> impl Content<TuiOut> + use<'a> {
|
||||
Fixed::y(2, self.project.view_track_names(self.color))
|
||||
}
|
||||
pub fn view_pool (&self) -> impl Content<TuiOut> + use<'_> {
|
||||
Fixed::x(20, Bsp::s(
|
||||
Fill::x(Align::w(FieldH(self.color, "Clip pool:", ""))),
|
||||
Fill::y(Align::n(Tui::bg(Rgb(0, 0, 0), Outer(true, Style::default().fg(Tui::g(96)))
|
||||
.enclose(PoolView(&self.project.pool)))))))
|
||||
.enclose(PoolView(&self.pool)))))))
|
||||
}
|
||||
pub fn view_samples_keys (&self) -> impl Content<TuiOut> + use<'_> {
|
||||
self.project.sampler().map(|s|s.view_list(true, self.editor().unwrap()))
|
||||
|
|
@ -142,133 +163,23 @@ impl App {
|
|||
self.project.sampler().map(|s|s.view_meters_output())
|
||||
}
|
||||
pub fn view_dialog (&self) -> impl Content<TuiOut> + use<'_> {
|
||||
When(self.dialog.is_some(), Bsp::b( "",
|
||||
self.dialog.as_ref().map(|dialog|Bsp::b("",
|
||||
Fixed::xy(70, 23, Tui::fg_bg(Rgb(255,255,255), Rgb(16,16,16), Bsp::b(
|
||||
Repeat(" "), Outer(true, Style::default().fg(Tui::g(96)))
|
||||
.enclose(self.dialog.as_ref().map(|dialog|match dialog {
|
||||
Dialog::Menu(_) =>
|
||||
self.view_dialog_menu().boxed(),
|
||||
Dialog::Help(offset) =>
|
||||
self.view_dialog_help(*offset).boxed(),
|
||||
Dialog::Browser(target, browser) =>
|
||||
self.view_dialog_browser(target, browser).boxed(),
|
||||
Dialog::Options =>
|
||||
self.view_dialog_options().boxed(),
|
||||
Dialog::Device(index) =>
|
||||
self.view_dialog_device(*index).boxed(),
|
||||
Dialog::Message(message) =>
|
||||
self.view_dialog_message(message).boxed(),
|
||||
}))
|
||||
)))
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn view_dialog_menu (&self) -> impl Content<TuiOut> {
|
||||
let options = ||["Projects", "Settings", "Help", "Quit"].iter();
|
||||
let option = |a,i|Tui::fg(Rgb(255,255,255), format!("{}", a));
|
||||
Bsp::s(Tui::bold(true, "tek!"), Bsp::s("", Map::south(1, options, option)))
|
||||
}
|
||||
pub fn view_dialog_help <'a> (&'a self, offset: usize) -> impl Content<TuiOut> + use<'a> {
|
||||
Bsp::s(Tui::bold(true, "Help"), Bsp::s("", Map::south(1,
|
||||
move||self.config.keys.layers.iter()
|
||||
.filter_map(|a|(a.0)(self).then_some(a.1))
|
||||
.flat_map(|a|a)
|
||||
.filter_map(|x|if let Value::Exp(_, iter)=x.value{ Some(iter) } else { None })
|
||||
.skip(offset)
|
||||
.take(20),
|
||||
|mut b,i|Fixed::x(60, Align::w(Bsp::e("(", Bsp::e(
|
||||
b.next().map(|t|Fixed::x(16, Align::w(Tui::fg(Rgb(64,224,0), format!("{}", t.value))))),
|
||||
Bsp::e(" ", Align::w(format!("{}", b.0.0.trim()))))))))))
|
||||
}
|
||||
pub fn view_dialog_device (&self, index: usize) -> impl Content<TuiOut> + use<'_> {
|
||||
let choices = ||self.device_kinds().iter();
|
||||
let choice = move|label, i|
|
||||
Fill::x(Tui::bg(if i == index { Rgb(64,128,32) } else { Rgb(0,0,0) },
|
||||
Bsp::e(if i == index { "[ " } else { " " },
|
||||
Bsp::w(if i == index { " ]" } else { " " },
|
||||
label))));
|
||||
Bsp::s(Tui::bold(true, "Add device"), Map::south(1, choices, choice))
|
||||
}
|
||||
pub fn view_dialog_message <'a> (&'a self, message: &'a Message) -> impl Content<TuiOut> + use<'a> {
|
||||
Bsp::s(message, Bsp::s("", "[ OK ]"))
|
||||
}
|
||||
pub fn view_dialog_browser <'a> (&'a self, target: &BrowserTarget, browser: &'a Browser) -> impl Content<TuiOut> + use<'a> {
|
||||
Bsp::s(
|
||||
Padding::xy(3, 1, Fill::x(Align::w(FieldV(
|
||||
self.color,
|
||||
match target {
|
||||
BrowserTarget::SaveProject => "Save project:",
|
||||
BrowserTarget::LoadProject => "Load project:",
|
||||
BrowserTarget::ImportSample(_) => "Import sample:",
|
||||
BrowserTarget::ExportSample(_) => "Export sample:",
|
||||
BrowserTarget::ImportClip(_) => "Import clip:",
|
||||
BrowserTarget::ExportClip(_) => "Export clip:",
|
||||
},
|
||||
Shrink::x(3, Fixed::y(1, Tui::fg(Tui::g(96), RepeatH("🭻")))))))),
|
||||
Outer(true, Style::default().fg(Tui::g(96)))
|
||||
.enclose(Fill::xy(browser)))
|
||||
}
|
||||
pub fn view_dialog_load <'a> (&'a self, browser: &'a Browser) -> impl Content<TuiOut> + use<'a> {
|
||||
Bsp::s(
|
||||
Fill::x(Align::w(Margin::xy(1, 1, Bsp::e(
|
||||
Tui::bold(true, " Load project: "),
|
||||
Shrink::x(3, Fixed::y(1, RepeatH("🭻"))))))),
|
||||
Outer(true, Style::default().fg(Tui::g(96)))
|
||||
.enclose(Fill::xy(browser)))
|
||||
}
|
||||
pub fn view_dialog_export <'a> (&'a self, browser: &'a Browser) -> impl Content<TuiOut> + use<'a> {
|
||||
Bsp::s(
|
||||
Fill::x(Align::w(Margin::xy(1, 1, Bsp::e(
|
||||
Tui::bold(true, " Export: "),
|
||||
Shrink::x(3, Fixed::y(1, RepeatH("🭻"))))))),
|
||||
Outer(true, Style::default().fg(Tui::g(96)))
|
||||
.enclose(Fill::xy(browser)))
|
||||
}
|
||||
pub fn view_dialog_import <'a> (&'a self, browser: &'a Browser) -> impl Content<TuiOut> + use<'a> {
|
||||
Bsp::s(
|
||||
Fill::x(Align::w(Margin::xy(1, 1, Bsp::e(
|
||||
Tui::bold(true, " Import: "),
|
||||
Shrink::x(3, Fixed::y(1, RepeatH("🭻"))))))),
|
||||
Outer(true, Style::default().fg(Tui::g(96)))
|
||||
.enclose(Fill::xy(browser)))
|
||||
}
|
||||
pub fn view_dialog_options <'a> (&'a self) -> impl Content<TuiOut> + use<'a> {
|
||||
"TODO"
|
||||
.enclose(dialog))))))
|
||||
}
|
||||
}
|
||||
|
||||
impl ScenesView for App {
|
||||
fn arrangement (&self) -> &Arrangement {
|
||||
&self.project
|
||||
}
|
||||
fn scenes_height (&self) -> u16 {
|
||||
fn h_scenes (&self) -> u16 {
|
||||
(self.height() as u16).saturating_sub(20)
|
||||
}
|
||||
fn width_side (&self) -> u16 {
|
||||
fn w_side (&self) -> u16 {
|
||||
20
|
||||
}
|
||||
fn width_mid (&self) -> u16 {
|
||||
(self.width() as u16).saturating_sub(self.width_side())
|
||||
fn w_mid (&self) -> u16 {
|
||||
(self.width() as u16).saturating_sub(self.w_side())
|
||||
}
|
||||
fn scene_selected (&self) -> Option<usize> {
|
||||
self.project.selection.scene()
|
||||
}
|
||||
fn track_selected (&self) -> Option<usize> {
|
||||
self.project.selection.track()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn heading <'a> (
|
||||
key: &'a str,
|
||||
label: &'a str,
|
||||
count: usize,
|
||||
content: impl Content<TuiOut> + Send + Sync + 'a,
|
||||
editing: bool,
|
||||
) -> impl Content<TuiOut> + 'a {
|
||||
let count = format!("{count}");
|
||||
Fill::xy(Align::w(Bsp::s(Fill::x(Align::w(button_3(key, label, count, editing))), content)))
|
||||
}
|
||||
|
||||
/// Clear a pre-allocated buffer, then write into it.
|
||||
|
|
|
|||
|
|
@ -56,11 +56,11 @@ pub enum LaunchMode {
|
|||
/// 🎧 Multi-track MIDI sequencer.
|
||||
Arranger {
|
||||
/// Number of scenes
|
||||
#[arg(short = 'y', long, default_value_t = 8)] scenes: usize,
|
||||
#[arg(short = 'y', long, default_value_t = 16)] scenes: usize,
|
||||
/// Number of tracks
|
||||
#[arg(short = 'x', long, default_value_t = 4)] tracks: usize,
|
||||
#[arg(short = 'x', long, default_value_t = 12)] tracks: usize,
|
||||
/// Width of tracks
|
||||
#[arg(short = 'w', long, default_value_t = 14)] track_width: usize,
|
||||
#[arg(short = 'w', long, default_value_t = 15)] track_width: usize,
|
||||
},
|
||||
/// TODO: A MIDI-controlled audio mixer
|
||||
Mixer,
|
||||
|
|
@ -122,9 +122,9 @@ impl Cli {
|
|||
jack: jack.clone(),
|
||||
config,
|
||||
color: ItemTheme::random(),
|
||||
editor: match self.mode {
|
||||
LaunchMode::Sequencer | LaunchMode::Groovebox => Some((&clip).into()),
|
||||
_ => None
|
||||
pool: match self.mode {
|
||||
LaunchMode::Sequencer | LaunchMode::Groovebox => (&clip).into(),
|
||||
_ => Default::default()
|
||||
},
|
||||
project: Arrangement {
|
||||
name: Default::default(),
|
||||
|
|
@ -136,9 +136,9 @@ impl Cli {
|
|||
selection: Selection::TrackClip { track: 0, scene: 0 },
|
||||
midi_ins,
|
||||
midi_outs,
|
||||
pool: match self.mode {
|
||||
LaunchMode::Sequencer | LaunchMode::Groovebox => (&clip).into(),
|
||||
_ => Default::default()
|
||||
editor: match self.mode {
|
||||
LaunchMode::Sequencer | LaunchMode::Groovebox => Some((&clip).into()),
|
||||
_ => None
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
|
|
|
|||
|
|
@ -13,6 +13,34 @@ impl Arrangement {
|
|||
|
||||
#[tengri_proc::command(Arrangement)]
|
||||
impl ArrangementCommand {
|
||||
fn home (arranger: &mut Arrangement) -> Perhaps<Self> {
|
||||
arranger.editor = None;
|
||||
Ok(None)
|
||||
}
|
||||
fn edit (arranger: &mut Arrangement) -> Perhaps<Self> {
|
||||
let selection = arranger.selection().clone();
|
||||
arranger.editor = if arranger.editor.is_some() {
|
||||
None
|
||||
} else {
|
||||
match selection {
|
||||
Selection::TrackClip { track, scene } => {
|
||||
let clip = &mut arranger.scenes_mut()[scene].clips[track];
|
||||
if clip.is_none() {
|
||||
//app.clip_auto_create();
|
||||
*clip = Some(Arc::new(RwLock::new(MidiClip::new(
|
||||
&format!("t{track:02}s{scene:02}"),
|
||||
false, 384, None, Some(ItemTheme::random())
|
||||
))));
|
||||
}
|
||||
clip.as_ref().map(|c|c.into())
|
||||
}
|
||||
_ => {
|
||||
None
|
||||
}
|
||||
}
|
||||
};
|
||||
Ok(None)
|
||||
}
|
||||
/// Set the selection
|
||||
fn select (arranger: &mut Arrangement, s: Selection) -> Perhaps<Self> {
|
||||
*arranger.selection_mut() = s;
|
||||
|
|
@ -129,6 +157,22 @@ impl ArrangementCommand {
|
|||
todo!("delegate");
|
||||
Ok(None)
|
||||
}
|
||||
fn output_add (arranger: &mut Arrangement) -> Perhaps<Self> {
|
||||
arranger.midi_outs.push(JackMidiOut::new(
|
||||
arranger.jack(),
|
||||
format!("/M{}", arranger.midi_outs.len() + 1),
|
||||
&[]
|
||||
)?);
|
||||
Ok(None)
|
||||
}
|
||||
fn input_add (arranger: &mut Arrangement) -> Perhaps<Self> {
|
||||
arranger.midi_ins.push(JackMidiIn::new(
|
||||
arranger.jack(),
|
||||
format!("M{}/", arranger.midi_ins.len() + 1),
|
||||
&[]
|
||||
)?);
|
||||
Ok(None)
|
||||
}
|
||||
fn scene_add (arranger: &mut Arrangement) -> Perhaps<Self> {
|
||||
let index = arranger.scene_add(None, None)?.0;
|
||||
*arranger.selection_mut() = match arranger.selection() {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ pub struct Arrangement {
|
|||
pub jack: Jack,
|
||||
/// Source of time
|
||||
pub clock: Clock,
|
||||
/// Allows one MIDI clip to be edited
|
||||
pub editor: Option<MidiEditor>,
|
||||
/// List of global midi inputs
|
||||
pub midi_ins: Vec<JackMidiIn>,
|
||||
/// List of global midi outputs
|
||||
|
|
@ -35,18 +37,19 @@ pub struct Arrangement {
|
|||
pub arranger: Arc<RwLock<Buffer>>,
|
||||
/// Display size
|
||||
pub size: Measure<TuiOut>,
|
||||
/// Contains all clips in arrangement
|
||||
pub pool: Pool,
|
||||
/// Display size of clips area
|
||||
pub inner_size: Measure<TuiOut>,
|
||||
}
|
||||
|
||||
has!(Jack: |self: Arrangement|self.jack);
|
||||
has!(Clock: |self: Arrangement|self.clock);
|
||||
has!(Selection: |self: Arrangement|self.selection);
|
||||
has!(Vec<JackMidiIn>: |self: Arrangement|self.midi_ins);
|
||||
has!(Vec<JackMidiOut>: |self: Arrangement|self.midi_outs);
|
||||
has!(Vec<Scene>: |self: Arrangement|self.scenes);
|
||||
has!(Vec<Track>: |self: Arrangement|self.tracks);
|
||||
has!(Measure<TuiOut>: |self: Arrangement|self.size);
|
||||
has!(Jack: |self: Arrangement|self.jack);
|
||||
has!(Clock: |self: Arrangement|self.clock);
|
||||
has!(Selection: |self: Arrangement|self.selection);
|
||||
has!(Vec<JackMidiIn>: |self: Arrangement|self.midi_ins);
|
||||
has!(Vec<JackMidiOut>: |self: Arrangement|self.midi_outs);
|
||||
has!(Vec<Scene>: |self: Arrangement|self.scenes);
|
||||
has!(Vec<Track>: |self: Arrangement|self.tracks);
|
||||
has!(Measure<TuiOut>: |self: Arrangement|self.size);
|
||||
has!(Option<MidiEditor>: |self: Arrangement|self.editor);
|
||||
maybe_has!(Track: |self: Arrangement|
|
||||
{ Has::<Selection>::get(self).track().map(|index|Has::<Vec<Track>>::get(self).get(index)).flatten() };
|
||||
{ Has::<Selection>::get(self).track().map(|index|Has::<Vec<Track>>::get_mut(self).get_mut(index)).flatten() });
|
||||
|
|
@ -63,11 +66,6 @@ impl Arrangement {
|
|||
pub fn w_sidebar (&self, is_editing: bool) -> u16 {
|
||||
self.w() / if is_editing { 16 } else { 8 } as u16
|
||||
}
|
||||
/// Width taken by all tracks.
|
||||
pub fn w_tracks (&self) -> u16 {
|
||||
self.tracks_with_sizes(&self.selection(), None).last()
|
||||
.map(|(_, _, _, x)|x as u16).unwrap_or(0)
|
||||
}
|
||||
/// Width available to display tracks.
|
||||
pub fn w_tracks_area (&self, is_editing: bool) -> u16 {
|
||||
self.w().saturating_sub(self.w_sidebar(is_editing))
|
||||
|
|
@ -76,14 +74,6 @@ impl Arrangement {
|
|||
pub fn h (&self) -> u16 {
|
||||
self.size.h() as u16
|
||||
}
|
||||
/// Height taken by all inputs.
|
||||
pub fn h_inputs (&self) -> u16 {
|
||||
self.midi_ins_with_sizes().last().map(|(_, _, _, _, y)|y as u16).unwrap_or(0)
|
||||
}
|
||||
/// Height taken by all outputs.
|
||||
pub fn h_outputs (&self) -> u16 {
|
||||
self.midi_outs_with_sizes().last().map(|(_, _, _, _, y)|y as u16).unwrap_or(0)
|
||||
}
|
||||
/// Height taken by visible device slots.
|
||||
pub fn h_devices (&self) -> u16 {
|
||||
2
|
||||
|
|
@ -169,7 +159,7 @@ impl Arrangement {
|
|||
mouts: &[PortConnect],
|
||||
) -> Usually<(usize, &mut Track)> {
|
||||
let name: Arc<str> = name.map_or_else(
|
||||
||format!("t{:02}", self.track_last).into(),
|
||||
||format!("trk{:02}", self.track_last).into(),
|
||||
|x|x.to_string().into()
|
||||
);
|
||||
self.track_last += 1;
|
||||
|
|
@ -212,22 +202,13 @@ impl Arrangement {
|
|||
}
|
||||
|
||||
impl ScenesView for Arrangement {
|
||||
fn arrangement (&self) -> &Arrangement {
|
||||
self
|
||||
}
|
||||
fn scenes_height (&self) -> u16 {
|
||||
fn h_scenes (&self) -> u16 {
|
||||
(self.height() as u16).saturating_sub(20)
|
||||
}
|
||||
fn width_side (&self) -> u16 {
|
||||
fn w_side (&self) -> u16 {
|
||||
(self.width() as u16 * 2 / 10).max(20)
|
||||
}
|
||||
fn width_mid (&self) -> u16 {
|
||||
(self.width() as u16).saturating_sub(2 * self.width_side()).max(40)
|
||||
}
|
||||
fn scene_selected (&self) -> Option<usize> {
|
||||
self.selection().scene()
|
||||
}
|
||||
fn track_selected (&self) -> Option<usize> {
|
||||
self.selection().track()
|
||||
fn w_mid (&self) -> u16 {
|
||||
(self.width() as u16).saturating_sub(2 * self.w_side()).max(40)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,23 +9,6 @@ pub trait HasScenes: Has<Vec<Scene>> + Send + Sync {
|
|||
fn scenes_mut (&mut self) -> &mut Vec<Scene> {
|
||||
Has::<Vec<Scene>>::get_mut(self)
|
||||
}
|
||||
fn scenes_with_sizes (
|
||||
&self,
|
||||
editing: bool,
|
||||
height: usize,
|
||||
larger: usize,
|
||||
selected_track: Option<usize>,
|
||||
selected_scene: Option<usize>,
|
||||
) -> impl ScenesSizes<'_> {
|
||||
let mut y = 0;
|
||||
self.scenes().iter().enumerate().map(move|(s, scene)|{
|
||||
let active = editing && selected_track.is_some() && selected_scene == Some(s);
|
||||
let height = if active { larger } else { height };
|
||||
let data = (s, scene, y, y + height);
|
||||
y += height;
|
||||
data
|
||||
})
|
||||
}
|
||||
/// Generate the default name for a new scene
|
||||
fn scene_default_name (&self) -> Arc<str> {
|
||||
format!("s{:3>}", self.scenes().len() + 1).into()
|
||||
|
|
|
|||
|
|
@ -44,25 +44,6 @@ pub trait HasTracks: Has<Vec<Track>> + Send + Sync {
|
|||
}
|
||||
}
|
||||
}
|
||||
/// Iterate over tracks with their corresponding sizes.
|
||||
fn tracks_with_sizes (&self, selection: &Selection, editor_width: Option<usize>)
|
||||
-> impl TracksSizes<'_>
|
||||
{
|
||||
let mut x = 0;
|
||||
let active_track = if let Some(width) = editor_width {
|
||||
selection.track()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
self.tracks().iter().enumerate().map(move |(index, track)|{
|
||||
let width = active_track
|
||||
.and_then(|_|editor_width)
|
||||
.unwrap_or(track.width.max(8));
|
||||
let data = (index, track, x, x + width);
|
||||
x += width + Self::TRACK_SPACING;
|
||||
data
|
||||
})
|
||||
}
|
||||
/// Spacing between tracks.
|
||||
const TRACK_SPACING: usize = 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,442 +1,354 @@
|
|||
use crate::*;
|
||||
|
||||
impl Content<TuiOut> for Arrangement {
|
||||
fn content (&self) -> impl Render<TuiOut> {
|
||||
let ins = |x|Bsp::n(self.view_inputs_0(), x);
|
||||
let tracks = |x|Bsp::s(self.view_tracks_0(), x);
|
||||
let devices = |x|Bsp::s(self.view_devices_0(), x);
|
||||
let outs = |x|Bsp::s(self.view_outputs_0(), x);
|
||||
let bg = |x|Tui::bg(Reset, x);
|
||||
//let track_scroll = |x|Bsp::s(&self.track_scroll, x);
|
||||
//let scene_scroll = |x|Bsp::e(&self.scene_scroll, x);
|
||||
self.size.of(outs(tracks(devices(ins(bg(self.view_scenes_clips(&None)))))))
|
||||
}
|
||||
}
|
||||
|
||||
impl Arrangement {
|
||||
/// Render input matrix.
|
||||
fn view_inputs_0 (&self) -> impl Content<TuiOut> + '_ {
|
||||
Tui::bg(Reset, Bsp::s(
|
||||
self.view_input_intos(),
|
||||
Bsp::s(self.view_input_routes(), self.view_input_ports()),
|
||||
))
|
||||
pub fn view_inputs <'a> (&'a self, theme: ItemTheme) -> impl Content<TuiOut> + 'a {
|
||||
let mut h = 0;
|
||||
for track in self.tracks().iter() {
|
||||
h = h.max(self.midi_ins.len() as u16);
|
||||
}
|
||||
let h = h + 1;
|
||||
self.view_track_row_section(
|
||||
theme,
|
||||
Bsp::s(
|
||||
Fixed::y(1, Fill::x(Align::w(button_3("i", "nput ", format!("{}", self.midi_ins.len()), false)))),
|
||||
Fixed::y(h - 1, Fill::x(Align::nw(Stack::south(move|add: &mut dyn FnMut(&dyn Render<TuiOut>)|{
|
||||
for (index, port) in self.midi_ins.iter().enumerate() {
|
||||
add(&Fill::x(Align::w(format!("·i{index:02} {}", port.name()))));
|
||||
}
|
||||
}))))),
|
||||
button_2("I", "+", false),
|
||||
Tui::bg(theme.darker.rgb, Align::w(Fill::x(
|
||||
Stack::east(move|add: &mut dyn FnMut(&dyn Render<TuiOut>)|{
|
||||
for (index, track, x1, x2) in self.tracks_with_sizes() {
|
||||
add(&Fixed::x(self.track_width(index, track),
|
||||
Stack::south(move|add: &mut dyn FnMut(&dyn Render<TuiOut>)|{
|
||||
let index = 0;
|
||||
add(&Fixed::y(1, track.sequencer.midi_ins.get(0).map(|port|
|
||||
Tui::fg_bg(Rgb(255, 255, 255), track.color.base.rgb,
|
||||
Fill::x(Align::w(format!("·i{index:02} {}", port.name())))))));
|
||||
for (index, port) in self.midi_ins().iter().enumerate() {
|
||||
add(&Fixed::y(1, Align::w(row!(
|
||||
Either(track.sequencer.monitoring, Tui::fg(Green, "●mon "), "·mon "),
|
||||
Either(track.sequencer.recording, Tui::fg(Red, "●rec "), "·rec "),
|
||||
Either(track.sequencer.overdub, Tui::fg(Yellow, "●dub "), "·dub "),
|
||||
))));
|
||||
}
|
||||
})))}})))))
|
||||
}
|
||||
fn view_input_ports (&self) -> impl Content<TuiOut> + '_ {
|
||||
let is_editing = false; //FIXME
|
||||
Tryptich::top(1)
|
||||
.left(20, button_3("i", "midi ins", format!("{}",
|
||||
self.midi_ins().len()), is_editing))
|
||||
.right(20, button_2("I", "add midi in", is_editing))
|
||||
.middle(self.width().saturating_sub(40) as u16,
|
||||
per_track_top(||self.tracks_with_sizes_scrolled(),
|
||||
move|t, track|{
|
||||
let rec = track.sequencer.recording;
|
||||
let mon = track.sequencer.monitoring;
|
||||
let rec = if rec { White } else { track.color.darkest.rgb };
|
||||
let mon = if mon { White } else { track.color.darkest.rgb };
|
||||
let bg = if self.track_selected() == Some(t) {
|
||||
track.color.light.rgb
|
||||
} else {
|
||||
track.color.base.rgb
|
||||
};
|
||||
//let bg2 = if t > 0 { track.color.base.rgb } else { Reset };
|
||||
wrap(bg, Tui::g(224), Tui::bold(true, Fill::x(Bsp::e(
|
||||
Tui::fg_bg(rec, bg, "Rec "),
|
||||
Tui::fg_bg(mon, bg, "Mon "),
|
||||
))))
|
||||
}))
|
||||
pub fn view_outputs <'a> (&'a self, theme: ItemTheme) -> impl Content<TuiOut> + 'a {
|
||||
let mut h = 1u16;
|
||||
for track in self.tracks().iter() {
|
||||
h = h.max(track.sequencer.midi_outs.len() as u16);
|
||||
}
|
||||
let h = h + 1;
|
||||
self.view_track_row_section(
|
||||
theme,
|
||||
Bsp::s(
|
||||
Fixed::y(1, Fill::x(Align::w(button_3("o", "utput", format!("{}", self.midi_outs.len()), false)))),
|
||||
Fixed::y(h - 1, Fill::xy(Align::nw(Stack::south(|add: &mut dyn FnMut(&dyn Render<TuiOut>)|{
|
||||
for (index, port) in self.midi_outs().iter().enumerate() {
|
||||
add(&Fill::x(Align::w(format!("·o{index:02} {}", port.name()))));
|
||||
}
|
||||
}))))),
|
||||
button_2("O", "+", false),
|
||||
Tui::bg(theme.darker.rgb, Align::w(Fill::x(
|
||||
Stack::east(move|add: &mut dyn FnMut(&dyn Render<TuiOut>)|{
|
||||
for (index, track, x1, x2) in self.tracks_with_sizes() {
|
||||
add(&Fixed::x(self.track_width(index, track),
|
||||
Stack::south(move|add: &mut dyn FnMut(&dyn Render<TuiOut>)|{
|
||||
let index = 0;
|
||||
add(&Fixed::y(1, track.sequencer.midi_outs.get(0).map(|port|
|
||||
Tui::fg_bg(Rgb(255, 255, 255), track.color.base.rgb,
|
||||
Fill::x(Align::w(format!("·o{index:02} {}", port.name())))))));
|
||||
for (index, port) in self.midi_outs().iter().enumerate() {
|
||||
add(&Fixed::y(1, Align::w(Bsp::e(
|
||||
Either(true, Tui::fg(Green, "●play "), "·play "),
|
||||
Either(false, Tui::fg(Yellow, "●solo "), "·solo "),
|
||||
))));
|
||||
}
|
||||
})));
|
||||
}
|
||||
})))))
|
||||
}
|
||||
fn view_input_routes (&self) -> impl Content<TuiOut> + '_ {
|
||||
Tryptich::top(self.h_inputs())
|
||||
.left(self.width_side(),
|
||||
io_ports(Tui::g(224), Tui::g(32), ||self.midi_ins_with_sizes()))
|
||||
.middle(self.width_mid(),
|
||||
per_track_top(||self.tracks_with_sizes_scrolled(),
|
||||
move|_, &Track { color, .. }|io_conns(
|
||||
color.dark.rgb, color.darker.rgb, ||self.midi_ins_with_sizes())))
|
||||
}
|
||||
fn view_input_intos (&self) -> impl Content<TuiOut> + '_ {
|
||||
Tryptich::top(2)
|
||||
.left(self.width_side(),
|
||||
Bsp::s(Align::e("Input:"), Align::e("Into clip:")))
|
||||
.middle(self.width_mid(),
|
||||
per_track_top(||self.tracks_with_sizes_scrolled(),
|
||||
|_, _|Tui::bg(Reset, Align::c(Bsp::s(OctaveVertical::default(), " ------ ")))))
|
||||
}
|
||||
/// Render output matrix.
|
||||
fn view_outputs_0 (&self) -> impl Content<TuiOut> + '_ {
|
||||
Tui::bg(Reset, Align::n(Bsp::s(
|
||||
Bsp::s(self.view_output_ports(), self.view_output_conns()),
|
||||
Bsp::s(self.view_output_nexts(), self.view_output_froms()),
|
||||
)))
|
||||
}
|
||||
fn view_output_ports (&self) -> impl Content<TuiOut> + '_ {
|
||||
Tryptich::top(1)
|
||||
.left(self.width_side(), self.view_output_count())
|
||||
.right(self.width_side(), self.view_output_add())
|
||||
.middle(self.width_mid(), self.view_output_map())
|
||||
}
|
||||
fn view_output_count (&self) -> impl Content<TuiOut> {
|
||||
button_3(
|
||||
"o",
|
||||
"midi outs",
|
||||
format!("{}", self.midi_outs().len()),
|
||||
false // self.is_editing()
|
||||
)
|
||||
}
|
||||
fn view_output_add (&self) -> impl Content<TuiOut> {
|
||||
button_2("O", "add midi out", false /* is_editing */)
|
||||
}
|
||||
fn view_output_map (&self) -> impl Content<TuiOut> + '_ {
|
||||
per_track_top(||self.tracks_with_sizes_scrolled(), move|i, t|{
|
||||
let mute = false;
|
||||
let solo = false;
|
||||
let mute = if mute { White } else { t.color.darkest.rgb };
|
||||
let solo = if solo { White } else { t.color.darkest.rgb };
|
||||
let bg_1 = if self.track_selected() == Some(i) {
|
||||
t.color.light.rgb
|
||||
} else {
|
||||
t.color.base.rgb
|
||||
};
|
||||
let bg_2 = if i > 0 { t.color.base.rgb } else { Reset };
|
||||
let mute = Tui::fg_bg(mute, bg_1, "Play ");
|
||||
let solo = Tui::fg_bg(solo, bg_1, "Solo ");
|
||||
wrap(bg_1, Tui::g(224), Tui::bold(true, Fill::x(Bsp::e(mute, solo))))
|
||||
})
|
||||
}
|
||||
fn view_output_conns (&self) -> impl Content<TuiOut> + '_ {
|
||||
Tryptich::top(self.h_outputs())
|
||||
.left(self.width_side(), io_ports(
|
||||
Tui::g(224), Tui::g(32), ||self.midi_outs_with_sizes()))
|
||||
.middle(self.width_mid(), per_track_top(||self.tracks_with_sizes_scrolled(),
|
||||
|_, t|io_conns(
|
||||
t.color.dark.rgb,
|
||||
t.color.darker.rgb,
|
||||
||self.midi_outs_with_sizes()
|
||||
)))
|
||||
}
|
||||
fn view_output_nexts (&self) -> impl Content<TuiOut> + '_ {
|
||||
Tryptich::top(2).left(self.width_side(), Align::ne("From clip:"))
|
||||
.middle(self.width_mid(), per_track_top(||self.tracks_with_sizes_scrolled(),
|
||||
|_, _|Tui::bg(Reset, Align::c(Bsp::s(" ------ ", OctaveVertical::default())))))
|
||||
}
|
||||
fn view_output_froms (&self) -> impl Content<TuiOut> + '_ {
|
||||
let label = Align::ne("Next clip:");
|
||||
Tryptich::top(2).left(self.width_side(), label)
|
||||
.middle(self.width_mid(), per_track_top(
|
||||
||self.tracks_with_sizes_scrolled(), |t, track|{
|
||||
let queued = track.sequencer.next_clip.is_some();
|
||||
let queued_blank = Thunk::new(||Tui::bg(Reset, " ------ "));
|
||||
let queued_clip = Thunk::new(||{
|
||||
Tui::bg(Reset, if let Some((_, clip)) = track.sequencer.next_clip.as_ref() {
|
||||
if let Some(clip) = clip {
|
||||
clip.read().unwrap().name.clone()
|
||||
} else {
|
||||
"Stop".into()
|
||||
}
|
||||
} else {
|
||||
"".into()
|
||||
})
|
||||
});
|
||||
Either(queued, queued_clip, queued_blank)
|
||||
}))
|
||||
}
|
||||
/// Render track headers
|
||||
fn view_tracks_0 (&self) -> impl Content<TuiOut> + '_ {
|
||||
let width_side = self.width_side();
|
||||
let width_mid = self.width_mid();
|
||||
let is_editing = false; // FIXME
|
||||
let track_selected = self.track_selected();
|
||||
Tryptich::center(3)
|
||||
.left(width_side,
|
||||
button_3("t", "track", format!("{}", self.tracks().len()), is_editing))
|
||||
.right(width_side, button_2("T", "add track", is_editing))
|
||||
.middle(width_mid, per_track(||self.tracks_with_sizes_scrolled(),
|
||||
move|index, track|wrap(
|
||||
if track_selected == Some(index) {
|
||||
track.color.light
|
||||
} else {
|
||||
track.color.base
|
||||
}.rgb,
|
||||
track.color.lightest.rgb,
|
||||
Tui::bold(true, Fill::xy(Align::nw(&track.name)))
|
||||
)))
|
||||
}
|
||||
/// Render device switches.
|
||||
fn view_devices_0 (&self) -> impl Content<TuiOut> + '_ {
|
||||
let width_side = self.width_side();
|
||||
let width_mid = self.width_mid();
|
||||
let is_editing = false; // FIXME
|
||||
let track_selected = self.track_selected();
|
||||
Tryptich::top(1)
|
||||
.left(width_side, button_3("d", "devices", format!("{}", 0), is_editing))
|
||||
.right(width_side, button_2("D", "add device", is_editing))
|
||||
.middle(width_mid, per_track_top(||self.tracks_with_sizes_scrolled(),
|
||||
move|index, track|{
|
||||
let bg = if track_selected == Some(index) {
|
||||
track.color.light
|
||||
} else {
|
||||
track.color.base
|
||||
};
|
||||
let fg = Tui::g(224);
|
||||
track.devices.get(0).map(|device|wrap(bg.rgb, fg, device.name()))
|
||||
}))
|
||||
pub fn view_track_devices <'a> (&'a self, theme: ItemTheme) -> impl Content<TuiOut> + 'a {
|
||||
let mut h = 2u16;
|
||||
for track in self.tracks().iter() {
|
||||
h = h.max(track.devices.len() as u16);
|
||||
}
|
||||
self.view_track_row_section(
|
||||
theme,
|
||||
button_3("d", "evice", format!("{}", self.track().map(|t|t.devices.len()).unwrap_or(0)), false),
|
||||
button_2("D", "+", false),
|
||||
Fixed::y(h, Tui::bg(theme.darker.rgb, Align::w(Fill::x(Stack::east(
|
||||
move|add: &mut dyn FnMut(&dyn Render<TuiOut>)|{
|
||||
for (index, track, x1, x2) in self.tracks_with_sizes() {
|
||||
add(&Fixed::xy(self.track_width(index, track), h + 1,
|
||||
Tui::bg(track.color.dark.rgb, Align::nw(Map::south(1, move||0..h,
|
||||
|_, index|format!("·d{index:02} {}", "--------"))))));
|
||||
}
|
||||
}))))))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> TracksView for T
|
||||
where T: HasSize<TuiOut> + HasTrackScroll + HasSelection + HasMidiIns {}
|
||||
pub trait HasClipsSize {
|
||||
fn clips_size (&self) -> &Measure<TuiOut>;
|
||||
}
|
||||
impl HasClipsSize for Arrangement {
|
||||
fn clips_size (&self) -> &Measure<TuiOut> { &self.inner_size }
|
||||
}
|
||||
|
||||
impl ClipsView for Arrangement {}
|
||||
impl TracksView for Arrangement {}
|
||||
|
||||
pub trait TracksView: HasSize<TuiOut> + HasTrackScroll + HasSelection + HasMidiIns {
|
||||
fn is_editing (&self) -> bool { false }
|
||||
impl<T: TracksView + ScenesView + Send + Sync> ClipsView for T {}
|
||||
|
||||
pub trait TracksView:
|
||||
ScenesView +
|
||||
HasMidiIns +
|
||||
HasMidiOuts +
|
||||
HasSize<TuiOut> +
|
||||
HasTrackScroll +
|
||||
HasSelection +
|
||||
HasEditor +
|
||||
HasClipsSize
|
||||
{
|
||||
fn tracks_width_available (&self) -> u16 {
|
||||
(self.width() as u16).saturating_sub(40)
|
||||
}
|
||||
fn tracks_with_sizes_scrolled <'t> (&'t self) -> impl TracksSizes<'t> {
|
||||
self.tracks_with_sizes(&self.selection(), self.is_editing().then_some(20/*FIXME*/))
|
||||
.map_while(move|(t, track, x1, x2)|
|
||||
((x2 as u16) < self.tracks_width_available())
|
||||
.then_some((t, track, x1, x2)))
|
||||
/// Iterate over tracks with their corresponding sizes.
|
||||
fn tracks_with_sizes (&self) -> impl TracksSizes<'_> {
|
||||
let editor_width = self.editor().map(|e|e.width());
|
||||
let active_track = self.selection().track();
|
||||
let mut x = 0;
|
||||
self.tracks().iter().enumerate().map_while(move |(index, track)|{
|
||||
let width = track.width.max(8);
|
||||
if x + width < self.clips_size().w() {
|
||||
let data = (index, track, x, x + width);
|
||||
x += width + Self::TRACK_SPACING;
|
||||
Some(data)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
fn view_track_row_section <'a> (
|
||||
&'a self,
|
||||
theme: ItemTheme,
|
||||
button: impl Content<TuiOut>,
|
||||
button_add: impl Content<TuiOut>,
|
||||
content: impl Content<TuiOut>
|
||||
) -> impl Content<TuiOut> {
|
||||
Bsp::w(
|
||||
Fill::y(Fixed::x(4, Align::nw(button_add))),
|
||||
Bsp::e(
|
||||
Fixed::x(20, Fill::y(Align::nw(button))),
|
||||
Fill::xy(Align::c(content))
|
||||
)
|
||||
)
|
||||
}
|
||||
fn view_track_header <'a, T: Content<TuiOut>> (
|
||||
&'a self, theme: ItemTheme, content: T
|
||||
) -> impl Content<TuiOut> {
|
||||
Fixed::x(12, Tui::bg(theme.darker.rgb, Fill::x(Align::e(content))))
|
||||
}
|
||||
fn view_track_names (&self, theme: ItemTheme) -> impl Content<TuiOut> {
|
||||
let content = Fixed::y(1, Align::w(Tui::bg(theme.darker.rgb, Align::w(Fill::x(
|
||||
Stack::east(move|add: &mut dyn FnMut(&dyn Render<TuiOut>)|{
|
||||
for (index, track, x1, x2) in self
|
||||
.tracks_with_sizes(&self.selection(), None)
|
||||
.skip(self.track_scroll())
|
||||
{
|
||||
add(&Fixed::x(track.width as u16,
|
||||
Tui::bg(if self.selection().track() == Some(index) {
|
||||
track.color.light.rgb
|
||||
} else {
|
||||
track.color.base.rgb
|
||||
}, Align::nw(Tui::fg(
|
||||
Rgb(255, 255, 255), Tui::bold(true,
|
||||
format!("{}", track.name)))))));
|
||||
}
|
||||
}))))));
|
||||
Bsp::w(
|
||||
self.view_track_header(theme, row!(
|
||||
Tui::bold(true, button_2("t", "rack ", false)),
|
||||
button_2("T", "+", false)
|
||||
)),
|
||||
content
|
||||
)
|
||||
self.view_track_row_section(
|
||||
theme,
|
||||
Bsp::s(
|
||||
button_3("t", "rack ", if let Some(track) = self.selection().track() {
|
||||
format!("{track}/{}", self.tracks().len())
|
||||
} else {
|
||||
format!("{}", self.tracks().len())
|
||||
}, false),
|
||||
button_3("s", "cene ", if let Some(scene) = self.selection().scene() {
|
||||
format!("{scene}/{}", self.scenes().len())
|
||||
} else {
|
||||
format!("{}", self.scenes().len())
|
||||
}, false)
|
||||
),
|
||||
Bsp::s(
|
||||
button_2("T", "+", false),
|
||||
button_2("S", "+", false),
|
||||
),
|
||||
Tui::bg(theme.darker.rgb, Fixed::y(2, Fill::x(
|
||||
Stack::east(move|add: &mut dyn FnMut(&dyn Render<TuiOut>)|{
|
||||
for (index, track, x1, x2) in self.tracks_with_sizes() {
|
||||
add(&Fixed::x(self.track_width(index, track),
|
||||
Tui::bg(if self.selection().track() == Some(index) {
|
||||
track.color.light.rgb
|
||||
} else {
|
||||
track.color.base.rgb
|
||||
}, Bsp::s(Fill::x(Align::nw(Bsp::e(
|
||||
format!("·t{index:02} "),
|
||||
Tui::fg(Rgb(255, 255, 255), Tui::bold(true, &track.name))
|
||||
))), ""))) );
|
||||
}
|
||||
})))))
|
||||
}
|
||||
fn view_track_outputs <'a> (&'a self, theme: ItemTheme) -> impl Content<TuiOut> {
|
||||
let mut max_outputs = 0u16;
|
||||
for track in self.tracks().iter() {
|
||||
max_outputs = max_outputs.max(track.sequencer.midi_outs.len() as u16);
|
||||
}
|
||||
let content = Align::w(Fixed::y(max_outputs + 1,
|
||||
fn view_track_outputs <'a> (&'a self, theme: ItemTheme, h: u16) -> impl Content<TuiOut> {
|
||||
self.view_track_row_section(theme,
|
||||
Bsp::s(
|
||||
Fill::x(Align::w(button_2("o", "utput", false))),
|
||||
Fill::xy(Stack::south(|add: &mut dyn FnMut(&dyn Render<TuiOut>)|{
|
||||
for port in self.midi_outs().iter() {
|
||||
add(&Fill::x(Align::w(port.name())));
|
||||
}
|
||||
}))),
|
||||
button_2("O", "+", false),
|
||||
Tui::bg(theme.darker.rgb, Align::w(Fill::x(
|
||||
Stack::east(move|add: &mut dyn FnMut(&dyn Render<TuiOut>)|{
|
||||
for (index, track, x1, x2) in self
|
||||
.tracks_with_sizes(&self.selection(), None)
|
||||
.skip(self.track_scroll())
|
||||
{
|
||||
(add)(&Fixed::x(track.width as u16, Align::nw(Bsp::s(
|
||||
Tui::bg(track.color.base.rgb, Fill::x(Align::w(format!("[mut] [sol]")))),
|
||||
Map::south(1, ||track.sequencer.midi_outs.iter(),
|
||||
for (index, track, x1, x2) in self.tracks_with_sizes() {
|
||||
add(&Fixed::x(self.track_width(index, track),
|
||||
Align::nw(Fill::y(Map::south(1, ||track.sequencer.midi_outs.iter(),
|
||||
|port, index|Tui::fg(Rgb(255, 255, 255),
|
||||
Tui::bg(track.color.dark.rgb, Fill::x(Align::w(
|
||||
format!("{index}: {}", port.name()))))))))));
|
||||
Fixed::y(1, Tui::bg(track.color.dark.rgb, Fill::x(Align::w(
|
||||
format!("·o{index:02} {}", port.name())))))))))));
|
||||
}
|
||||
}))))));
|
||||
Bsp::w(
|
||||
self.view_track_header(theme, row!(
|
||||
Tui::bold(true, button_2("o", "utput", false)),
|
||||
button_2("O", "+", false)
|
||||
)),
|
||||
content
|
||||
)
|
||||
})))))
|
||||
}
|
||||
fn view_track_inputs <'a> (&'a self, theme: ItemTheme) -> impl Content<TuiOut> {
|
||||
let mut h = 0u16;
|
||||
for track in self.tracks().iter() {
|
||||
h = h.max(track.sequencer.midi_ins.len() as u16);
|
||||
}
|
||||
let content = Tui::bg(theme.darker.rgb, Align::w(Fill::x(
|
||||
Stack::east(move|add: &mut dyn FnMut(&dyn Render<TuiOut>)|{
|
||||
for (index, track, x1, x2) in self
|
||||
.tracks_with_sizes(&self.selection(), None)
|
||||
.skip(self.track_scroll())
|
||||
{
|
||||
add(&Fixed::xy(track.width as u16, h + 1,
|
||||
Align::nw(Bsp::s(
|
||||
Tui::bg(track.color.base.rgb,
|
||||
Fill::x(Align::w(format!("[rec] [mon]")))),
|
||||
Map::south(1, ||track.sequencer.midi_ins.iter(),
|
||||
|port, index|Tui::fg_bg(Rgb(255, 255, 255), track.color.dark.rgb,
|
||||
Fill::x(Align::w(format!("{index}: {}", port.name())))))))));
|
||||
}
|
||||
|
||||
}))));
|
||||
Bsp::w(
|
||||
self.view_track_header(theme, row!(
|
||||
Tui::bold(true, button_2("i", "nputs", false)),
|
||||
button_2("I", "+", false)
|
||||
)),
|
||||
Fixed::y(h, Fill::x(Align::w(Fixed::y(h + 1, content)))),
|
||||
)
|
||||
self.view_track_row_section(
|
||||
theme,
|
||||
button_2("i", "nput", false),
|
||||
button_2("I", "+", false),
|
||||
Tui::bg(theme.darker.rgb, Align::w(Fill::x(
|
||||
Stack::east(move|add: &mut dyn FnMut(&dyn Render<TuiOut>)|{
|
||||
for (index, track, x1, x2) in self.tracks_with_sizes() {
|
||||
add(&Fixed::xy(self.track_width(index, track), h + 1,
|
||||
Align::nw(Bsp::s(
|
||||
Tui::bg(track.color.base.rgb,
|
||||
Fill::x(Align::w(row!(
|
||||
Either(track.sequencer.monitoring, Tui::fg(Green, "●mon "), "·mon "),
|
||||
Either(track.sequencer.recording, Tui::fg(Red, "●rec "), "·rec "),
|
||||
Either(track.sequencer.overdub, Tui::fg(Yellow, "●dub "), "·dub "),
|
||||
)))),
|
||||
Map::south(1, ||track.sequencer.midi_ins.iter(),
|
||||
|port, index|Tui::fg_bg(Rgb(255, 255, 255), track.color.dark.rgb,
|
||||
Fill::x(Align::w(format!("·i{index:02} {}", port.name())))))))));
|
||||
}})))))
|
||||
}
|
||||
fn view_track_devices <'a> (&'a self, theme: ItemTheme) -> impl Content<TuiOut> {
|
||||
let mut h = 2u16;
|
||||
for track in self.tracks().iter() {
|
||||
h = h.max(track.devices.len() as u16);
|
||||
}
|
||||
Bsp::w(
|
||||
self.view_track_header(theme, row!(
|
||||
Tui::bold(true, button_2("d", "evice", false)),
|
||||
button_2("D", "+", false)
|
||||
)),
|
||||
Fixed::y(h, Tui::bg(theme.darker.rgb, Align::w(Fill::x(Stack::east(
|
||||
move|add: &mut dyn FnMut(&dyn Render<TuiOut>)|{
|
||||
for (index, track, x1, x2) in self
|
||||
.tracks_with_sizes(&self.selection(), None)
|
||||
.skip(self.track_scroll())
|
||||
{
|
||||
add(&Fixed::xy(track.width as u16, h + 1,
|
||||
Tui::bg(track.color.dark.rgb, Align::nw(Map::south(1, move||0..h,
|
||||
|_, index|format!("{index}: {}", "--------"))))));
|
||||
}
|
||||
}))))))
|
||||
}
|
||||
fn view_track_header <'a, T: Content<TuiOut>> (
|
||||
&'a self, theme: ItemTheme, content: T
|
||||
) -> impl Content<TuiOut> {
|
||||
Fixed::x(20, Tui::bg(theme.darker.rgb, Fill::x(Align::e(content))))
|
||||
fn track_width (&self, index: usize, track: &Track) -> u16 {
|
||||
(if self.selection().track() == Some(index)
|
||||
&& let Some(editor) = self.editor()
|
||||
{
|
||||
editor.width().max(24).max(track.width)
|
||||
} else {
|
||||
track.width
|
||||
}) as u16
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ScenesView: HasSelection + HasSceneScroll + Send + Sync {
|
||||
pub trait ScenesView:
|
||||
HasEditor +
|
||||
HasSelection +
|
||||
HasSceneScroll +
|
||||
HasClipsSize +
|
||||
Send +
|
||||
Sync
|
||||
{
|
||||
/// Default scene height.
|
||||
const H_SCENE: usize = 2;
|
||||
const H_SCENE: usize = 2;
|
||||
/// Default editor height.
|
||||
const H_EDITOR: usize = 15;
|
||||
fn arrangement (&self) -> &Arrangement;
|
||||
fn scene_selected (&self) -> Option<usize>;
|
||||
fn track_selected (&self) -> Option<usize>;
|
||||
fn scenes_height (&self) -> u16;
|
||||
fn width_side (&self) -> u16;
|
||||
fn width_mid (&self) -> u16;
|
||||
fn h_scenes (&self) -> u16;
|
||||
fn w_side (&self) -> u16;
|
||||
fn w_mid (&self) -> u16;
|
||||
fn scenes_with_sizes (&self) -> impl ScenesSizes<'_> {
|
||||
let editing = self.editor().is_some();
|
||||
let height = Self::H_SCENE;
|
||||
let larger = 8;//FIXME//self.editor().map(|e|e.height()).unwrap_or(Self::H_SCENE);
|
||||
let selected_track = self.selection().track();
|
||||
let selected_scene = self.selection().scene();
|
||||
let mut y = 0;
|
||||
self.scenes().iter().enumerate().skip(self.scene_scroll()).map_while(move|(s, scene)|{
|
||||
let active = editing && selected_track.is_some() && selected_scene == Some(s);
|
||||
let height = if active { larger } else { height };
|
||||
if y + height <= self.clips_size().h() {
|
||||
let data = (s, scene, y, y + height);
|
||||
y += height;
|
||||
Some(data)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
fn view_scenes_names (&self) -> impl Content<TuiOut> {
|
||||
Stack::south(move|add: &mut dyn FnMut(&dyn Render<TuiOut>)|{
|
||||
for (index, scene) in self.scenes().iter().enumerate().skip(self.scene_scroll()) {
|
||||
for (index, scene, ..) in self.scenes_with_sizes() {
|
||||
add(&self.view_scene_name(index, scene));
|
||||
}
|
||||
})
|
||||
}
|
||||
fn view_scene_name (&self, index: usize, scene: &Scene) -> impl Content<TuiOut> {
|
||||
Fixed::xy(20, 2, Tui::bg(if self.selection().scene() == Some(index) {
|
||||
let h = if self.selection().scene() == Some(index) && let Some(editor) = self.editor() {
|
||||
(editor.height() as u16).max(12)
|
||||
} else {
|
||||
Self::H_SCENE as u16
|
||||
};
|
||||
let bg = if self.selection().scene() == Some(index) {
|
||||
scene.color.light.rgb
|
||||
} else {
|
||||
scene.color.base.rgb
|
||||
}, Align::nw(Bsp::e(
|
||||
format!(" {index:2} "),
|
||||
Tui::fg(Rgb(255, 255, 255),
|
||||
Tui::bold(true, format!("{}", scene.name)))))))
|
||||
//let height = (1 + y2 - y1) as u16;
|
||||
//let name = Some(scene.name.clone());
|
||||
//let content = Fill::x(Align::w(Tui::bold(true, Bsp::e(" ⯈ ", name))));
|
||||
//let selected = self.scene_selected() == Some(s);
|
||||
//let neighbor = s > 0 && self.scene_selected() == Some(s - 1);
|
||||
//let is_last = self.scenes().len().saturating_sub(1) == s;
|
||||
//let theme = scene.color;
|
||||
//let fg = theme.lightest.rgb;
|
||||
//let bg = if selected { theme.light } else { theme.base }.rgb;
|
||||
//let hi = if let Some(previous) = previous {
|
||||
//if neighbor { previous.light.rgb } else { previous.base.rgb }
|
||||
//} else {
|
||||
//Reset
|
||||
//};
|
||||
//let lo = if is_last { Reset } else if selected {
|
||||
//theme.light.rgb
|
||||
//} else {
|
||||
//theme.base.rgb
|
||||
//};
|
||||
//add(&Fill::x(map_south(y1 as u16, height, Fixed::y(height, Phat {
|
||||
//width: 0, height: 0, content, colors: [fg, bg, hi, lo]
|
||||
//}))))
|
||||
//}
|
||||
}
|
||||
fn scenes_with_prev_color (&self) -> impl Iterator<Item=SceneWith<Option<ItemTheme>>> + Send + Sync {
|
||||
self.scenes_iter().map(|(s, scene, y1, y2)|(s, scene, y1, y2,
|
||||
(s>0).then(||self.arrangement().scenes()[s-1].color)))
|
||||
}
|
||||
fn per_track <'a, T: Content<TuiOut> + 'a, U: TracksSizes<'a>> (
|
||||
tracks: impl Fn() -> U + Send + Sync + 'a,
|
||||
callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a
|
||||
) -> impl Content<TuiOut> + 'a {
|
||||
Map::new(tracks, move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|{
|
||||
Tui::fg_bg(track.color.lightest.rgb, track.color.darker.rgb,
|
||||
map_east(x1 as u16, (x2 - x1) as u16, callback(index, track))) })
|
||||
}
|
||||
fn scenes_with_clip (&self, track_index: usize) -> impl Iterator<Item=SceneWith<'_, Option<ItemTheme>>> + Send + Sync {
|
||||
self.scenes_iter().map(move|(s, scene, y1, y2)|(s, scene, y1, y2,
|
||||
(s>0).then(||self.arrangement().scenes()[s-1].clips[track_index].as_ref()
|
||||
.map(|c|c.read().unwrap().color)
|
||||
.unwrap_or(ItemTheme::G[32]))))
|
||||
}
|
||||
/// A scene with size and color.
|
||||
fn scenes_iter (&self) -> impl Iterator<Item=(usize, &Scene, usize, usize,)> + Send + Sync {
|
||||
let selection = Has::<Selection>::get(self.arrangement());
|
||||
self.arrangement().scenes_with_sizes(
|
||||
false, // FIXME self.is_editing(),
|
||||
Self::H_SCENE, Self::H_EDITOR,
|
||||
selection.track(), selection.scene(),
|
||||
).map_while(|(s, scene, y1, y2)|(y2<=self.scenes_height() as usize)
|
||||
.then_some((s, scene, y1, y2)))
|
||||
}
|
||||
/// Height required to display all scenes.
|
||||
fn scenes_height_total (&self, is_editing: bool) -> u16 {
|
||||
self.scenes_with_sizes(
|
||||
is_editing,
|
||||
Self::H_SCENE,
|
||||
Self::H_EDITOR,
|
||||
self.selection().track(),
|
||||
self.selection().scene(),
|
||||
)
|
||||
.last()
|
||||
.map(|(_, _, _, y)|y as u16).unwrap_or(0)
|
||||
};
|
||||
Fixed::xy(20, h, Tui::bg(bg, Align::nw(Bsp::s(
|
||||
Fill::x(Align::w(Bsp::e(
|
||||
format!("·s{index:02} "),
|
||||
Tui::fg(Rgb(255, 255, 255), Tui::bold(true, &scene.name))
|
||||
))),
|
||||
When(self.selection().scene() == Some(index) && self.is_editing(),
|
||||
Fill::xy(Align::nw(Bsp::s(
|
||||
self.editor().as_ref().map(|e|e.clip_status()),
|
||||
self.editor().as_ref().map(|e|e.edit_status())))))))))
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ClipsView: TracksView + ScenesView + Send + Sync {
|
||||
fn view_scenes_clips <'a> (&'a self, editor: &'a Option<MidiEditor>)
|
||||
pub trait ClipsView:
|
||||
TracksView +
|
||||
ScenesView +
|
||||
HasClipsSize +
|
||||
Send +
|
||||
Sync
|
||||
{
|
||||
fn view_scenes_clips <'a> (&'a self)
|
||||
-> impl Content<TuiOut> + 'a
|
||||
{
|
||||
Fill::xy(Stack::<TuiOut, _>::east(move|column: &mut dyn FnMut(&dyn Render<TuiOut>)|{
|
||||
for (track_index, track, _, _) in self
|
||||
.tracks_with_sizes(&self.selection(), None)
|
||||
.skip(self.track_scroll())
|
||||
{
|
||||
//column(&Fixed::x(5, Fill::xy(Tui::bg(Green, "kyp"))));
|
||||
column(&Fixed::x(
|
||||
track.width as u16,
|
||||
Fill::y(self.view_track_clips(track_index, track))
|
||||
))
|
||||
}
|
||||
}))
|
||||
self.clips_size().of(Fill::xy(Bsp::a(
|
||||
Fill::xy(Align::se(Tui::fg(Green, format!("{}x{}", self.clips_size().w(), self.clips_size().h())))),
|
||||
Stack::<TuiOut, _>::east(move|column: &mut dyn FnMut(&dyn Render<TuiOut>)|{
|
||||
for (track_index, track, _, _) in self.tracks_with_sizes() {
|
||||
//column(&Fixed::x(5, Fill::xy(Tui::bg(Green, "kyp"))));
|
||||
column(&Fixed::x(
|
||||
if self.selection().track() == Some(track_index)
|
||||
&& let Some(editor) = self.editor()
|
||||
{
|
||||
editor.width().max(24).max(track.width)
|
||||
} else {
|
||||
track.width
|
||||
} as u16,
|
||||
Fill::y(self.view_track_clips(track_index, track))
|
||||
))
|
||||
}
|
||||
}))))
|
||||
}
|
||||
|
||||
fn view_track_clips <'a> (&'a self, track_index: usize, track: &Track) -> impl Content<TuiOut> {
|
||||
fn view_track_clips <'a> (&'a self, track_index: usize, track: &'a Track) -> impl Content<TuiOut> + 'a {
|
||||
Stack::south(move|cell: &mut dyn FnMut(&dyn Render<TuiOut>)|{
|
||||
for (scene_index, scene) in self.scenes().iter().enumerate().skip(self.scene_scroll()) {
|
||||
let (name, theme) = if let Some(Some(clip)) = &scene.clips.get(track_index) {
|
||||
for (scene_index, scene, ..) in self.scenes_with_sizes() {
|
||||
let (name, theme): (Arc<str>, ItemTheme) = if let Some(Some(clip)) = &scene.clips.get(track_index) {
|
||||
let clip = clip.read().unwrap();
|
||||
(Some(clip.name.clone()), clip.color)
|
||||
(format!(" ⏹ {}", &clip.name).into(), clip.color)
|
||||
} else {
|
||||
(None, ItemTheme::G[32])
|
||||
(" ⏹ -- ".into(), ItemTheme::G[32])
|
||||
};
|
||||
let fg = theme.lightest.rgb;
|
||||
let mut outline = theme.base.rgb;
|
||||
let bg = if self.selection().track() == Some(track_index)
|
||||
&& self.selection().scene() == Some(scene_index)
|
||||
{
|
||||
outline = theme.lightest.rgb;
|
||||
outline = theme.lighter.rgb;
|
||||
theme.light.rgb
|
||||
} else if self.selection().track() == Some(track_index)
|
||||
|| self.selection().scene() == Some(scene_index)
|
||||
|
|
@ -446,83 +358,33 @@ pub trait ClipsView: TracksView + ScenesView + Send + Sync {
|
|||
} else {
|
||||
theme.dark.rgb
|
||||
};
|
||||
cell(&Fixed::xy(track.width as u16, 2, Bsp::b(
|
||||
let w = if self.selection().track() == Some(track_index)
|
||||
&& let Some(editor) = self.editor ()
|
||||
{
|
||||
editor.width().max(24).max(track.width)
|
||||
} else {
|
||||
track.width
|
||||
} as u16;
|
||||
let y = if self.selection().scene() == Some(scene_index)
|
||||
&& let Some(editor) = self.editor ()
|
||||
{
|
||||
editor.height().max(12)
|
||||
} else {
|
||||
Self::H_SCENE
|
||||
} as u16;
|
||||
cell(&Fixed::xy(w, y, Bsp::b(
|
||||
Fill::xy(Outer(true, Style::default().fg(outline))),
|
||||
Fill::xy(Align::nw(Tui::fg_bg(fg, bg, Align::nw(name.unwrap_or(" ---- ".into()))))))));
|
||||
//let (name, theme) = if let Some(clip) = &scene.clips.get(track_index).flatten() {
|
||||
//let clip = clip.read().unwrap();
|
||||
//(Some(clip.name.clone()), clip.color)
|
||||
//} else {
|
||||
//(None, ItemTheme::G[32])
|
||||
//};
|
||||
//let content = Fill::x(Align::w(Tui::bold(true, Bsp::e(" ⏹ ", name))));
|
||||
//let same_track = self.track_selected() == Some(track_index);
|
||||
//let selected = same_track && self.scene_selected() == Some(s);
|
||||
//let neighbor = same_track && s > 0 && self.scene_selected() == Some(s - 1);
|
||||
//let is_last = self.scenes().len().saturating_sub(1) == s;
|
||||
//let fg = theme.lightest.rgb;
|
||||
//let bg = if selected { theme.light } else { theme.base }.rgb;
|
||||
//let hi = if let Some(previous) = previous {
|
||||
//if neighbor { previous.light.rgb } else { previous.base.rgb }
|
||||
//} else {
|
||||
//Reset
|
||||
//};
|
||||
//let lo = if is_last {
|
||||
//Reset
|
||||
//} else if selected {
|
||||
//theme.light.rgb
|
||||
//} else {
|
||||
//theme.base.rgb
|
||||
//};
|
||||
//let height = (1 + y2 - y1) as u16;
|
||||
//let is_editing = false; //FIXME
|
||||
//let editor = (); //FIXME
|
||||
//cell(&Fixed::xy(track.width as u16, 2, Bsp::b(Fixed::y(height, Phat {
|
||||
//width: 0, height: 0, content, colors: [fg, bg, hi, lo]
|
||||
//}), When(
|
||||
//is_editing && same_track && self.scene_selected() == Some(s),
|
||||
//editor
|
||||
//))))
|
||||
Fill::xy(Bsp::b(
|
||||
Bsp::b(
|
||||
Tui::fg_bg(outline, bg, Fill::xy("")),
|
||||
Fill::xy(Align::nw(Tui::fg_bg(fg, bg, Tui::bold(true, name)))),
|
||||
),
|
||||
Fill::xy(When(self.selection().track() == Some(track_index)
|
||||
&& self.selection().scene() == Some(scene_index)
|
||||
&& self.is_editing(), self.editor())))))));
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn scenes_clips_2 <'a> (
|
||||
&'a self,
|
||||
theme: ItemTheme
|
||||
) -> impl Content<TuiOut> + 'a {
|
||||
Fixed::y(self.scenes().len() as u16 * 2, Tui::bg(theme.darker.rgb,
|
||||
Align::w(Fill::x(Map::new(||self.scenes().iter().skip(self.scene_scroll()),
|
||||
move|scene: &'a Scene, index|self.track_scenes(index, scene))))))
|
||||
}
|
||||
fn track_scenes <'a> (
|
||||
&'a self,
|
||||
scene_index: usize,
|
||||
scene: &'a Scene
|
||||
) -> impl Content<TuiOut> + 'a {
|
||||
Push::y(scene_index as u16 * 2u16, Fixed::xy(20, 2, Map::new(
|
||||
move||scene.clips.iter().skip(self.track_scroll()),
|
||||
move|clip: &'a Option<Arc<RwLock<MidiClip>>>, track_index|
|
||||
self.track_scene_clip(scene_index, scene, track_index, clip))))
|
||||
}
|
||||
fn track_scene_clip (
|
||||
&self,
|
||||
scene_index: usize,
|
||||
scene: &Scene,
|
||||
track_index: usize,
|
||||
clip: &Option<Arc<RwLock<MidiClip>>>
|
||||
) -> impl Content<TuiOut> {
|
||||
let (theme, text) = if let Some(clip) = clip {
|
||||
let clip = clip.read().unwrap();
|
||||
(clip.color, clip.name.clone())
|
||||
} else {
|
||||
(scene.color, Default::default())
|
||||
};
|
||||
Push::x(track_index as u16 * 14, Tui::bg(theme.dark.rgb, Bsp::e(
|
||||
format!(" {scene_index:2} {track_index:2} "),
|
||||
Tui::fg(Rgb(255, 255, 255),
|
||||
Tui::bold(true, format!("{}", text))))))
|
||||
}
|
||||
}
|
||||
|
||||
pub trait HasWidth {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,16 @@ use crate::*;
|
|||
use std::path::PathBuf;
|
||||
use std::ffi::OsString;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum BrowserTarget {
|
||||
SaveProject,
|
||||
LoadProject,
|
||||
ImportSample(Arc<RwLock<Option<Sample>>>),
|
||||
ExportSample(Arc<RwLock<Option<Sample>>>),
|
||||
ImportClip(Arc<RwLock<Option<MidiClip>>>),
|
||||
ExportClip(Arc<RwLock<Option<MidiClip>>>),
|
||||
}
|
||||
|
||||
/// Browses for phrase to import/export
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Browser {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,12 @@
|
|||
use crate::*;
|
||||
|
||||
pub fn device_kinds () -> &'static [&'static str] {
|
||||
&[
|
||||
"Sampler",
|
||||
"Plugin (LV2)",
|
||||
]
|
||||
}
|
||||
|
||||
impl<T: Has<Vec<Device>> + Has<Track>> HasDevices for T {
|
||||
fn devices (&self) -> &Vec<Device> {
|
||||
self.get()
|
||||
|
|
|
|||
113
crates/device/src/dialog.rs
Normal file
113
crates/device/src/dialog.rs
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
use crate::*;
|
||||
|
||||
/// Various possible dialog overlays
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Dialog {
|
||||
Help(usize),
|
||||
Menu(usize),
|
||||
Device(usize),
|
||||
Message(Message),
|
||||
Browser(BrowserTarget, Browser),
|
||||
Options,
|
||||
}
|
||||
|
||||
/// Various possible messages
|
||||
#[derive(PartialEq, Clone, Copy, Debug)]
|
||||
pub enum Message {
|
||||
FailedToAddDevice,
|
||||
}
|
||||
|
||||
content!(TuiOut: |self: Message| match self {
|
||||
Self::FailedToAddDevice => "Failed to add device."
|
||||
});
|
||||
|
||||
content!(TuiOut: |self: Dialog| match self {
|
||||
Self::Menu(_) =>
|
||||
self.view_dialog_menu().boxed(),
|
||||
Self::Help(offset) =>
|
||||
self.view_dialog_help(*offset).boxed(),
|
||||
Self::Browser(target, browser) =>
|
||||
self.view_dialog_browser(target, browser).boxed(),
|
||||
Self::Options =>
|
||||
self.view_dialog_options().boxed(),
|
||||
Self::Device(index) =>
|
||||
self.view_dialog_device(*index).boxed(),
|
||||
Self::Message(message) =>
|
||||
self.view_dialog_message(message).boxed(),
|
||||
});
|
||||
|
||||
impl Dialog {
|
||||
pub fn view_dialog_menu (&self) -> impl Content<TuiOut> {
|
||||
let options = ||["Projects", "Settings", "Help", "Quit"].iter();
|
||||
let option = |a,i|Tui::fg(Rgb(255,255,255), format!("{}", a));
|
||||
Bsp::s(Tui::bold(true, "tek!"), Bsp::s("", Map::south(1, options, option)))
|
||||
}
|
||||
pub fn view_dialog_help <'a> (&'a self, offset: usize) -> impl Content<TuiOut> + use<'a> {
|
||||
Bsp::s(Tui::bold(true, "Help"), "FIXME")
|
||||
//Bsp::s(Tui::bold(true, "Help"), Bsp::s("", Map::south(1,
|
||||
//move||self.config.keys.layers.iter()
|
||||
//.filter_map(|a|(a.0)(self).then_some(a.1))
|
||||
//.flat_map(|a|a)
|
||||
//.filter_map(|x|if let Value::Exp(_, iter)=x.value{ Some(iter) } else { None })
|
||||
//.skip(offset)
|
||||
//.take(20),
|
||||
//|mut b,i|Fixed::x(60, Align::w(Bsp::e("(", Bsp::e(
|
||||
//b.next().map(|t|Fixed::x(16, Align::w(Tui::fg(Rgb(64,224,0), format!("{}", t.value))))),
|
||||
//Bsp::e(" ", Align::w(format!("{}", b.0.0.trim()))))))))))
|
||||
}
|
||||
pub fn view_dialog_device (&self, index: usize) -> impl Content<TuiOut> + use<'_> {
|
||||
let choices = ||device_kinds().iter();
|
||||
let choice = move|label, i|
|
||||
Fill::x(Tui::bg(if i == index { Rgb(64,128,32) } else { Rgb(0,0,0) },
|
||||
Bsp::e(if i == index { "[ " } else { " " },
|
||||
Bsp::w(if i == index { " ]" } else { " " },
|
||||
label))));
|
||||
Bsp::s(Tui::bold(true, "Add device"), Map::south(1, choices, choice))
|
||||
}
|
||||
pub fn view_dialog_message <'a> (&'a self, message: &'a Message) -> impl Content<TuiOut> + use<'a> {
|
||||
Bsp::s(message, Bsp::s("", "[ OK ]"))
|
||||
}
|
||||
pub fn view_dialog_browser <'a> (&'a self, target: &BrowserTarget, browser: &'a Browser) -> impl Content<TuiOut> + use<'a> {
|
||||
Bsp::s(
|
||||
Padding::xy(3, 1, Fill::x(Align::w(FieldV(
|
||||
Default::default(),
|
||||
match target {
|
||||
BrowserTarget::SaveProject => "Save project:",
|
||||
BrowserTarget::LoadProject => "Load project:",
|
||||
BrowserTarget::ImportSample(_) => "Import sample:",
|
||||
BrowserTarget::ExportSample(_) => "Export sample:",
|
||||
BrowserTarget::ImportClip(_) => "Import clip:",
|
||||
BrowserTarget::ExportClip(_) => "Export clip:",
|
||||
},
|
||||
Shrink::x(3, Fixed::y(1, Tui::fg(Tui::g(96), RepeatH("🭻")))))))),
|
||||
Outer(true, Style::default().fg(Tui::g(96)))
|
||||
.enclose(Fill::xy(browser)))
|
||||
}
|
||||
pub fn view_dialog_load <'a> (&'a self, browser: &'a Browser) -> impl Content<TuiOut> + use<'a> {
|
||||
Bsp::s(
|
||||
Fill::x(Align::w(Margin::xy(1, 1, Bsp::e(
|
||||
Tui::bold(true, " Load project: "),
|
||||
Shrink::x(3, Fixed::y(1, RepeatH("🭻"))))))),
|
||||
Outer(true, Style::default().fg(Tui::g(96)))
|
||||
.enclose(Fill::xy(browser)))
|
||||
}
|
||||
pub fn view_dialog_export <'a> (&'a self, browser: &'a Browser) -> impl Content<TuiOut> + use<'a> {
|
||||
Bsp::s(
|
||||
Fill::x(Align::w(Margin::xy(1, 1, Bsp::e(
|
||||
Tui::bold(true, " Export: "),
|
||||
Shrink::x(3, Fixed::y(1, RepeatH("🭻"))))))),
|
||||
Outer(true, Style::default().fg(Tui::g(96)))
|
||||
.enclose(Fill::xy(browser)))
|
||||
}
|
||||
pub fn view_dialog_import <'a> (&'a self, browser: &'a Browser) -> impl Content<TuiOut> + use<'a> {
|
||||
Bsp::s(
|
||||
Fill::x(Align::w(Margin::xy(1, 1, Bsp::e(
|
||||
Tui::bold(true, " Import: "),
|
||||
Shrink::x(3, Fixed::y(1, RepeatH("🭻"))))))),
|
||||
Outer(true, Style::default().fg(Tui::g(96)))
|
||||
.enclose(Fill::xy(browser)))
|
||||
}
|
||||
pub fn view_dialog_options <'a> (&'a self) -> impl Content<TuiOut> + use<'a> {
|
||||
"TODO"
|
||||
}
|
||||
}
|
||||
|
|
@ -103,12 +103,24 @@ impl MidiViewer for MidiEditor {
|
|||
fn set_clip (&mut self, p: Option<&Arc<RwLock<MidiClip>>>) { self.mode.set_clip(p) }
|
||||
}
|
||||
|
||||
pub trait HasEditor {
|
||||
fn editor (&self) -> Option<&MidiEditor>;
|
||||
fn editor_mut (&mut self) -> Option<&mut MidiEditor>;
|
||||
fn is_editing (&self) -> bool { self.editor().is_some() }
|
||||
fn editor_w (&self) -> usize { 0 }
|
||||
fn editor_h (&self) -> usize { 0 }
|
||||
impl<T: Has<Option<MidiEditor>>> HasEditor for T {}
|
||||
|
||||
pub trait HasEditor: Has<Option<MidiEditor>> {
|
||||
fn editor (&self) -> Option<&MidiEditor> {
|
||||
self.get().as_ref()
|
||||
}
|
||||
fn editor_mut (&mut self) -> Option<&mut MidiEditor> {
|
||||
self.get_mut().as_mut()
|
||||
}
|
||||
fn is_editing (&self) -> bool {
|
||||
self.editor().is_some()
|
||||
}
|
||||
fn editor_w (&self) -> usize {
|
||||
self.editor().map(|e|e.size.w()).unwrap_or(0)
|
||||
}
|
||||
fn editor_h (&self) -> usize {
|
||||
self.editor().map(|e|e.size.h()).unwrap_or(0)
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export] macro_rules! has_editor {
|
||||
|
|
|
|||
|
|
@ -13,9 +13,9 @@ impl MidiEditor {
|
|||
(clip.color, clip.name.clone(), clip.length, clip.looped)
|
||||
} else { (ItemTheme::G[64], String::new().into(), 0, false) };
|
||||
Fixed::x(20, col!(
|
||||
Fill::x(Align::w(FieldV(color, "Clip ", format!("{name}")))),
|
||||
Fill::x(Align::w(FieldH(color, "Length", format!("{length}")))),
|
||||
Fill::x(Align::w(FieldH(color, "Loop ", looped.to_string()))),
|
||||
Fill::x(Align::w(Bsp::e(" Clip ", format!("{name}")))),
|
||||
Fill::x(Align::w(Bsp::e(" Length ", format!("{length}")))),
|
||||
Fill::x(Align::w(Bsp::e(" Loop ", looped.to_string()))),
|
||||
))
|
||||
}
|
||||
|
||||
|
|
@ -31,9 +31,9 @@ impl MidiEditor {
|
|||
let note_pos = format!("{:>3}", note_pos);
|
||||
let note_len = format!("{:>4}", self.get_note_len());
|
||||
Fixed::x(20, col!(
|
||||
Fill::x(Align::w(FieldH(color, "Time", format!("{length}/{time_zoom}+{time_pos}")))),
|
||||
Fill::x(Align::w(FieldH(color, "Lock", format!("{time_lock}")))),
|
||||
Fill::x(Align::w(FieldH(color, "Note", format!("{note_name} {note_pos} {note_len}")))),
|
||||
Fill::x(Align::w(Bsp::e(" Time ", format!("{length}/{time_zoom}+{time_pos}")))),
|
||||
Fill::x(Align::w(Bsp::e(" Lock ", format!("{time_lock}")))),
|
||||
Fill::x(Align::w(Bsp::e(" Note ", format!("{note_name} {note_pos} {note_len}")))),
|
||||
))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -46,19 +46,19 @@ pub(crate) fn note_y_iter (note_lo: usize, note_hi: usize, y0: u16)
|
|||
(note_lo..=note_hi).rev().enumerate().map(move|(y, n)|(y, y0 + y as u16, n))
|
||||
}
|
||||
|
||||
content!(TuiOut:|self: PianoHorizontal| Tui::bg(Tui::g(40), Bsp::s(
|
||||
content!(TuiOut:|self: PianoHorizontal| Bsp::s(
|
||||
Bsp::e(
|
||||
Fixed::x(5, format!("{}x{}", self.size.w(), self.size.h())),
|
||||
self.timeline()
|
||||
),
|
||||
Bsp::e(
|
||||
self.keys(),
|
||||
self.size.of(Tui::bg(Tui::g(32), Bsp::b(
|
||||
self.size.of(Bsp::b(
|
||||
Fill::xy(self.notes()),
|
||||
Fill::xy(self.cursor()),
|
||||
)))
|
||||
))
|
||||
),
|
||||
)));
|
||||
));
|
||||
|
||||
impl PianoHorizontal {
|
||||
/// Draw the piano roll background.
|
||||
|
|
|
|||
|
|
@ -14,54 +14,27 @@ pub(crate) use std::error::Error;
|
|||
pub(crate) use std::ffi::OsString;
|
||||
|
||||
pub(crate) use ::tengri::{from, has, maybe_has, Usually, Perhaps, Has, MaybeHas};
|
||||
pub(crate) use ::tengri::{dsl::*, input::*, output::*, tui::{*, ratatui::prelude::*}};
|
||||
pub(crate) use ::tengri::{dsl::*, input::*, output::{*, Margin}, tui::{*, ratatui::prelude::*}};
|
||||
pub(crate) use ::tek_engine::*;
|
||||
pub(crate) use ::tek_engine::midi::{u7, LiveEvent, MidiMessage};
|
||||
pub(crate) use ::tek_engine::jack::{Control, ProcessScope, MidiWriter, RawMidi};
|
||||
pub(crate) use ratatui::{prelude::Rect, widgets::{Widget, canvas::{Canvas, Line}}};
|
||||
pub(crate) use Color::*;
|
||||
|
||||
mod device;
|
||||
pub use self::device::*;
|
||||
mod device; pub use self::device::*;
|
||||
mod dialog; pub use self::dialog::*;
|
||||
|
||||
#[cfg(feature = "arranger")] mod arranger;
|
||||
#[cfg(feature = "arranger")] pub use self::arranger::*;
|
||||
|
||||
#[cfg(feature = "browser")] mod browser;
|
||||
#[cfg(feature = "browser")] pub use self::browser::*;
|
||||
|
||||
#[cfg(feature = "clock")] mod clock;
|
||||
#[cfg(feature = "clock")] pub use self::clock::*;
|
||||
|
||||
#[cfg(feature = "editor")] mod editor;
|
||||
#[cfg(feature = "editor")] pub use self::editor::*;
|
||||
|
||||
#[cfg(feature = "pool")] mod pool;
|
||||
#[cfg(feature = "pool")] pub use self::pool::*;
|
||||
|
||||
#[cfg(feature = "sequencer")] mod sequencer;
|
||||
#[cfg(feature = "sequencer")] pub use self::sequencer::*;
|
||||
|
||||
#[cfg(feature = "sampler")] mod sampler;
|
||||
#[cfg(feature = "sampler")] pub use self::sampler::*;
|
||||
|
||||
#[cfg(feature = "meter")] mod meter;
|
||||
#[cfg(feature = "meter")] pub use self::meter::*;
|
||||
|
||||
#[cfg(feature = "mixer")] mod mixer;
|
||||
#[cfg(feature = "mixer")] pub use self::mixer::*;
|
||||
|
||||
#[cfg(feature = "lv2")] mod lv2;
|
||||
#[cfg(feature = "lv2")] pub use self::lv2::*;
|
||||
|
||||
#[cfg(feature = "sf2")] mod sf2;
|
||||
#[cfg(feature = "sf2")] pub use self::sf2::*;
|
||||
|
||||
#[cfg(feature = "vst2")] mod vst2;
|
||||
#[cfg(feature = "vst2")] pub use self::vst2::*;
|
||||
|
||||
#[cfg(feature = "vst3")] mod vst3;
|
||||
#[cfg(feature = "vst3")] pub use self::vst3::*;
|
||||
|
||||
#[cfg(feature = "clap")] mod clap;
|
||||
#[cfg(feature = "clap")] pub use self::clap::*;
|
||||
#[cfg(feature = "arranger")] mod arranger; #[cfg(feature = "arranger")] pub use self::arranger::*;
|
||||
#[cfg(feature = "browser")] mod browser; #[cfg(feature = "browser")] pub use self::browser::*;
|
||||
#[cfg(feature = "clock")] mod clock; #[cfg(feature = "clock")] pub use self::clock::*;
|
||||
#[cfg(feature = "editor")] mod editor; #[cfg(feature = "editor")] pub use self::editor::*;
|
||||
#[cfg(feature = "pool")] mod pool; #[cfg(feature = "pool")] pub use self::pool::*;
|
||||
#[cfg(feature = "sequencer")] mod sequencer; #[cfg(feature = "sequencer")] pub use self::sequencer::*;
|
||||
#[cfg(feature = "sampler")] mod sampler; #[cfg(feature = "sampler")] pub use self::sampler::*;
|
||||
#[cfg(feature = "meter")] mod meter; #[cfg(feature = "meter")] pub use self::meter::*;
|
||||
#[cfg(feature = "mixer")] mod mixer; #[cfg(feature = "mixer")] pub use self::mixer::*;
|
||||
#[cfg(feature = "lv2")] mod lv2; #[cfg(feature = "lv2")] pub use self::lv2::*;
|
||||
#[cfg(feature = "sf2")] mod sf2; #[cfg(feature = "sf2")] pub use self::sf2::*;
|
||||
#[cfg(feature = "vst2")] mod vst2; #[cfg(feature = "vst2")] pub use self::vst2::*;
|
||||
#[cfg(feature = "vst3")] mod vst3; #[cfg(feature = "vst3")] pub use self::vst3::*;
|
||||
#[cfg(feature = "clap")] mod clap; #[cfg(feature = "clap")] pub use self::clap::*;
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ impl Default for Sequencer {
|
|||
play_clip: None,
|
||||
next_clip: None,
|
||||
recording: false,
|
||||
monitoring: false,
|
||||
monitoring: true,
|
||||
overdub: false,
|
||||
|
||||
notes_in: RwLock::new([false;128]).into(),
|
||||
|
|
|
|||
2
deps/tengri
vendored
2
deps/tengri
vendored
|
|
@ -1 +1 @@
|
|||
Subproject commit a55e84c29f51606e0996f7f88b7664ca0d37365b
|
||||
Subproject commit f21781e81664e1991e3985e2377becca9c1d58cf
|
||||
Loading…
Add table
Add a link
Reference in a new issue