mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-06 03:36:41 +01:00
This commit is contained in:
parent
b0ef0cfd21
commit
fa73821a0b
29 changed files with 831 additions and 650 deletions
|
|
@ -13,10 +13,9 @@
|
|||
(keys
|
||||
(layer-if :focus-message "./keys_message.edn")
|
||||
(layer-if :focus-device-add "./keys_device_add.edn")
|
||||
(layer-if :focus-pool-import "./keys_pool_file.edn")
|
||||
(layer-if :focus-pool-export "./keys_pool_file.edn")
|
||||
(layer-if :focus-pool-rename "./keys_clip_rename.edn")
|
||||
(layer-if :focus-pool-length "./keys_clip_length.edn")
|
||||
(layer-if :focus-browser "./keys_browser")
|
||||
(layer-if :focus-pool-rename "./keys_rename.edn")
|
||||
(layer-if :focus-pool-length "./keys_length.edn")
|
||||
(layer "./keys_global.edn")
|
||||
(layer-if :focus-editor "./keys_editor.edn")
|
||||
(layer-if :focus-clip "./keys_clip.edn")
|
||||
|
|
|
|||
|
|
@ -15,10 +15,9 @@
|
|||
(fill/y :view-editor)))))))))))
|
||||
|
||||
(keys
|
||||
(layer-if :focus-pool-import "./keys_pool_file.edn")
|
||||
(layer-if :focus-pool-export "./keys_pool_file.edn")
|
||||
(layer-if :focus-pool-rename "./keys_clip_rename.edn")
|
||||
(layer-if :focus-pool-length "./keys_clip_length.edn")
|
||||
(layer-if :focus-browser "./keys_browser")
|
||||
(layer-if :focus-pool-rename "./keys_rename.edn")
|
||||
(layer-if :focus-pool-length "./keys_length.edn")
|
||||
(layer "./keys_global.edn")
|
||||
(layer "./keys_clock.edn")
|
||||
(layer "./keys_editor.edn")
|
||||
|
|
|
|||
|
|
@ -11,10 +11,9 @@
|
|||
:view-editor)))))
|
||||
|
||||
(keys
|
||||
(layer-if :mode-pool-import "./keys_pool_file.edn")
|
||||
(layer-if :mode-pool-export "./keys_pool_file.edn")
|
||||
(layer-if :mode-pool-rename "./keys_clip_rename.edn")
|
||||
(layer-if :mode-pool-length "./keys_clip_length.edn")
|
||||
(layer-if :focus-browser "./keys_browser")
|
||||
(layer-if :mode-pool-rename "./keys_rename.edn")
|
||||
(layer-if :mode-pool-length "./keys_length.edn")
|
||||
(layer "./keys_global.edn")
|
||||
(layer "./keys_editor.edn")
|
||||
(layer "./keys_clock.edn")
|
||||
|
|
|
|||
8
config/keys_browser.edn
Normal file
8
config/keys_browser.edn
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
(@escape browser cancel)
|
||||
(@return browser confirm)
|
||||
(@up browser set-cursor :browser-cursor-prev)
|
||||
(@down browser set-cursor :browser-cursor-next)
|
||||
(@right browser set-address :browser-address-selected)
|
||||
(@left browser set-address :browser-address-parent)
|
||||
(:char browser append-to-search ;char)
|
||||
(@backspace browser delete-from-search :last)
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
(@esc device cancel)
|
||||
(@up device pick :device-kind-prev)
|
||||
(@down device pick :device-kind-next)
|
||||
(@enter device add :device-kind)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
(@esc toggle-dialog :dialog-menu)
|
||||
(@esc cancel-dialog)
|
||||
(@f1 toggle-dialog :dialog-help)
|
||||
(@f6 toggle-dialog :dialog-save)
|
||||
(@f8 toggle-dialog :dialog-options)
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ impl HasClips for ExampleClips {
|
|||
}
|
||||
}
|
||||
fn main () -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut clips = MidiPool::default();//ExampleClips(Arc::new(vec![].into()));
|
||||
let mut clips = Pool::default();//ExampleClips(Arc::new(vec![].into()));
|
||||
PoolClipCommand::Import {
|
||||
index: 0,
|
||||
path: std::path::PathBuf::from("./example.mid")
|
||||
|
|
|
|||
|
|
@ -184,44 +184,15 @@ handle!(TuiIn: |self: App, input|Ok(if let Some(command) = self.config.keys.comm
|
|||
}
|
||||
}
|
||||
|
||||
#[tengri_proc::expose] impl MidiPool {
|
||||
fn _todo_bool_stub (&self) -> bool {
|
||||
todo!()
|
||||
}
|
||||
fn _todo_path_buf_stub (&self) -> PathBuf {
|
||||
todo!()
|
||||
}
|
||||
fn _todo_arc_str_stub (&self) -> Arc<str> {
|
||||
todo!()
|
||||
}
|
||||
fn clip_new (&self) -> MidiClip {
|
||||
self.new_clip()
|
||||
}
|
||||
fn clip_cloned (&self) -> MidiClip {
|
||||
self.cloned_clip()
|
||||
}
|
||||
fn clip_index_current (&self) -> usize {
|
||||
0
|
||||
}
|
||||
fn clip_index_after (&self) -> usize {
|
||||
0
|
||||
}
|
||||
fn clip_index_previous (&self) -> usize {
|
||||
0
|
||||
}
|
||||
fn clip_index_next (&self) -> usize {
|
||||
0
|
||||
}
|
||||
fn color_random (&self) -> ItemColor {
|
||||
ItemColor::random()
|
||||
}
|
||||
}
|
||||
|
||||
#[tengri_proc::command(App)] impl AppCommand {
|
||||
fn toggle_dialog (app: &mut App, dialog: Dialog) -> Perhaps<Self> {
|
||||
app.toggle_dialog(Some(dialog));
|
||||
Ok(None)
|
||||
}
|
||||
fn cancel_dialog (app: &mut App) -> Perhaps<Self> {
|
||||
app.toggle_dialog(None);
|
||||
Ok(None)
|
||||
}
|
||||
fn toggle_editor (app: &mut App, value: bool) -> Perhaps<Self> {
|
||||
app.toggle_editor(Some(value));
|
||||
Ok(None)
|
||||
|
|
@ -489,275 +460,3 @@ impl<'state> Context<'state, SamplerCommand> for App {
|
|||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
#[tengri_proc::command(MidiPool)] impl PoolCommand {
|
||||
/// Toggle visibility of pool
|
||||
fn show (pool: &mut MidiPool, visible: bool) -> Perhaps<Self> {
|
||||
pool.visible = visible;
|
||||
Ok(Some(Self::Show { visible: !visible }))
|
||||
}
|
||||
/// Select a clip from the clip pool
|
||||
fn select (pool: &mut MidiPool, index: usize) -> Perhaps<Self> {
|
||||
pool.set_clip_index(index);
|
||||
Ok(None)
|
||||
}
|
||||
/// Rename a clip
|
||||
fn rename (pool: &mut MidiPool, command: ClipRenameCommand) -> Perhaps<Self> {
|
||||
Ok(match command {
|
||||
ClipRenameCommand::Begin => {
|
||||
pool.begin_clip_rename();
|
||||
None
|
||||
},
|
||||
_ => command.delegate(pool, |command|Self::Rename{command})?
|
||||
})
|
||||
}
|
||||
/// Change the length of a clip
|
||||
fn length (pool: &mut MidiPool, command: ClipLengthCommand) -> Perhaps<Self> {
|
||||
Ok(match command {
|
||||
ClipLengthCommand::Begin => {
|
||||
pool.begin_clip_length();
|
||||
None
|
||||
},
|
||||
_ => command.delegate(pool, |command|Self::Length{command})?
|
||||
})
|
||||
}
|
||||
/// Import from file
|
||||
fn import (pool: &mut MidiPool, command: FileBrowserCommand) -> Perhaps<Self> {
|
||||
Ok(match command {
|
||||
FileBrowserCommand::Begin => {
|
||||
pool.begin_import();
|
||||
None
|
||||
},
|
||||
_ => command.delegate(pool, |command|Self::Import{command})?
|
||||
})
|
||||
}
|
||||
/// Export to file
|
||||
fn export (pool: &mut MidiPool, command: FileBrowserCommand) -> Perhaps<Self> {
|
||||
Ok(match command {
|
||||
FileBrowserCommand::Begin => {
|
||||
pool.begin_export();
|
||||
None
|
||||
},
|
||||
_ => command.delegate(pool, |command|Self::Export{command})?
|
||||
})
|
||||
}
|
||||
/// Update the contents of the clip pool
|
||||
fn clip (pool: &mut MidiPool, command: PoolClipCommand) -> Perhaps<Self> {
|
||||
Ok(command.execute(pool)?.map(|command|Self::Clip{command}))
|
||||
}
|
||||
}
|
||||
|
||||
#[tengri_proc::command(MidiPool)] impl PoolClipCommand {
|
||||
fn add (pool: &mut MidiPool, index: usize, clip: MidiClip) -> Perhaps<Self> {
|
||||
let mut index = index;
|
||||
let clip = Arc::new(RwLock::new(clip));
|
||||
let mut clips = pool.clips_mut();
|
||||
if index >= clips.len() {
|
||||
index = clips.len();
|
||||
clips.push(clip)
|
||||
} else {
|
||||
clips.insert(index, clip);
|
||||
}
|
||||
Ok(Some(Self::Delete { index }))
|
||||
}
|
||||
fn delete (pool: &mut MidiPool, index: usize) -> Perhaps<Self> {
|
||||
let clip = pool.clips_mut().remove(index).read().unwrap().clone();
|
||||
Ok(Some(Self::Add { index, clip }))
|
||||
}
|
||||
fn swap (pool: &mut MidiPool, index: usize, other: usize) -> Perhaps<Self> {
|
||||
pool.clips_mut().swap(index, other);
|
||||
Ok(Some(Self::Swap { index, other }))
|
||||
}
|
||||
fn import (pool: &mut MidiPool, index: usize, path: PathBuf) -> Perhaps<Self> {
|
||||
let bytes = std::fs::read(&path)?;
|
||||
let smf = Smf::parse(bytes.as_slice())?;
|
||||
let mut t = 0u32;
|
||||
let mut events = vec![];
|
||||
for track in smf.tracks.iter() {
|
||||
for event in track.iter() {
|
||||
t += event.delta.as_int();
|
||||
if let TrackEventKind::Midi { channel, message } = event.kind {
|
||||
events.push((t, channel.as_int(), message));
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut clip = MidiClip::new("imported", true, t as usize + 1, None, None);
|
||||
for event in events.iter() {
|
||||
clip.notes[event.0 as usize].push(event.2);
|
||||
}
|
||||
Ok(Self::Add { index, clip }.execute(pool)?)
|
||||
}
|
||||
fn export (pool: &mut MidiPool, index: usize, path: PathBuf) -> Perhaps<Self> {
|
||||
todo!("export clip to midi file");
|
||||
}
|
||||
fn set_name (pool: &mut MidiPool, index: usize, name: Arc<str>) -> Perhaps<Self> {
|
||||
let clip = &mut pool.clips_mut()[index];
|
||||
let old_name = clip.read().unwrap().name.clone();
|
||||
clip.write().unwrap().name = name;
|
||||
Ok(Some(Self::SetName { index, name: old_name }))
|
||||
}
|
||||
fn set_length (pool: &mut MidiPool, index: usize, length: usize) -> Perhaps<Self> {
|
||||
let clip = &mut pool.clips_mut()[index];
|
||||
let old_len = clip.read().unwrap().length;
|
||||
clip.write().unwrap().length = length;
|
||||
Ok(Some(Self::SetLength { index, length: old_len }))
|
||||
}
|
||||
fn set_color (pool: &mut MidiPool, index: usize, color: ItemColor) -> Perhaps<Self> {
|
||||
let mut color = ItemTheme::from(color);
|
||||
std::mem::swap(&mut color, &mut pool.clips()[index].write().unwrap().color);
|
||||
Ok(Some(Self::SetColor { index, color: color.base }))
|
||||
}
|
||||
}
|
||||
|
||||
#[tengri_proc::command(MidiPool)] impl ClipRenameCommand {
|
||||
fn begin (pool: &mut MidiPool) -> Perhaps<Self> {
|
||||
unreachable!();
|
||||
}
|
||||
fn cancel (pool: &mut MidiPool) -> Perhaps<Self> {
|
||||
if let Some(PoolMode::Rename(clip, ref mut old_name)) = pool.mode_mut().clone() {
|
||||
pool.clips()[clip].write().unwrap().name = old_name.clone().into();
|
||||
}
|
||||
return Ok(None)
|
||||
}
|
||||
fn confirm (pool: &mut MidiPool) -> Perhaps<Self> {
|
||||
if let Some(PoolMode::Rename(clip, ref mut old_name)) = pool.mode_mut().clone() {
|
||||
let old_name = old_name.clone();
|
||||
*pool.mode_mut() = None;
|
||||
return Ok(Some(Self::Set { value: old_name }))
|
||||
}
|
||||
return Ok(None)
|
||||
}
|
||||
fn set (pool: &mut MidiPool, value: Arc<str>) -> Perhaps<Self> {
|
||||
if let Some(PoolMode::Rename(clip, ref mut old_name)) = pool.mode_mut().clone() {
|
||||
pool.clips()[clip].write().unwrap().name = value;
|
||||
}
|
||||
return Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
#[tengri_proc::command(MidiPool)] impl ClipLengthCommand {
|
||||
fn begin (pool: &mut MidiPool) -> Perhaps<Self> {
|
||||
unreachable!()
|
||||
}
|
||||
fn cancel (pool: &mut MidiPool) -> Perhaps<Self> {
|
||||
if let Some(PoolMode::Length(..)) = pool.mode_mut().clone() {
|
||||
*pool.mode_mut() = None;
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
fn set (pool: &mut MidiPool, length: usize) -> Perhaps<Self> {
|
||||
if let Some(PoolMode::Length(clip, ref mut length, ref mut focus))
|
||||
= pool.mode_mut().clone()
|
||||
{
|
||||
let old_length;
|
||||
{
|
||||
let clip = pool.clips()[clip].clone();//.write().unwrap();
|
||||
old_length = Some(clip.read().unwrap().length);
|
||||
clip.write().unwrap().length = *length;
|
||||
}
|
||||
*pool.mode_mut() = None;
|
||||
return Ok(old_length.map(|length|Self::Set { length }))
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
fn next (pool: &mut MidiPool) -> Perhaps<Self> {
|
||||
if let Some(PoolMode::Length(clip, ref mut length, ref mut focus))
|
||||
= pool.mode_mut().clone()
|
||||
{
|
||||
focus.next()
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
fn prev (pool: &mut MidiPool) -> Perhaps<Self> {
|
||||
if let Some(PoolMode::Length(clip, ref mut length, ref mut focus))
|
||||
= pool.mode_mut().clone()
|
||||
{
|
||||
focus.prev()
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
fn inc (pool: &mut MidiPool) -> Perhaps<Self> {
|
||||
if let Some(PoolMode::Length(clip, ref mut length, ref mut focus))
|
||||
= pool.mode_mut().clone()
|
||||
{
|
||||
match focus {
|
||||
ClipLengthFocus::Bar => { *length += 4 * PPQ },
|
||||
ClipLengthFocus::Beat => { *length += PPQ },
|
||||
ClipLengthFocus::Tick => { *length += 1 },
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
fn dec (pool: &mut MidiPool) -> Perhaps<Self> {
|
||||
if let Some(PoolMode::Length(clip, ref mut length, ref mut focus))
|
||||
= pool.mode_mut().clone()
|
||||
{
|
||||
match focus {
|
||||
ClipLengthFocus::Bar => { *length = length.saturating_sub(4 * PPQ) },
|
||||
ClipLengthFocus::Beat => { *length = length.saturating_sub(PPQ) },
|
||||
ClipLengthFocus::Tick => { *length = length.saturating_sub(1) },
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
#[tengri_proc::command(MidiPool)] impl FileBrowserCommand {
|
||||
fn begin (pool: &mut MidiPool) -> Perhaps<Self> {
|
||||
unreachable!();
|
||||
}
|
||||
fn cancel (pool: &mut MidiPool) -> Perhaps<Self> {
|
||||
pool.mode = None;
|
||||
Ok(None)
|
||||
}
|
||||
fn confirm (pool: &mut MidiPool) -> Perhaps<Self> {
|
||||
Ok(match pool.mode {
|
||||
Some(PoolMode::Import(index, ref mut browser)) => {
|
||||
if browser.is_file() {
|
||||
let path = browser.path();
|
||||
pool.mode = None;
|
||||
let _undo = PoolClipCommand::import(pool, index, path)?;
|
||||
None
|
||||
} else if browser.is_dir() {
|
||||
pool.mode = Some(PoolMode::Import(index, browser.chdir()?));
|
||||
None
|
||||
} else {
|
||||
None
|
||||
}
|
||||
},
|
||||
Some(PoolMode::Export(index, ref mut browser)) => {
|
||||
todo!()
|
||||
},
|
||||
_ => unreachable!(),
|
||||
})
|
||||
}
|
||||
fn select (pool: &mut MidiPool, index: usize) -> Perhaps<Self> {
|
||||
Ok(match pool.mode {
|
||||
Some(PoolMode::Import(index, ref mut browser)) => {
|
||||
browser.index = index;
|
||||
None
|
||||
},
|
||||
Some(PoolMode::Export(index, ref mut browser)) => {
|
||||
browser.index = index;
|
||||
None
|
||||
},
|
||||
_ => unreachable!(),
|
||||
})
|
||||
}
|
||||
fn chdir (pool: &mut MidiPool, dir: PathBuf) -> Perhaps<Self> {
|
||||
Ok(match pool.mode {
|
||||
Some(PoolMode::Import(index, ref mut browser)) => {
|
||||
pool.mode = Some(PoolMode::Import(index, FileBrowser::new(Some(dir))?));
|
||||
None
|
||||
},
|
||||
Some(PoolMode::Export(index, ref mut browser)) => {
|
||||
pool.mode = Some(PoolMode::Export(index, FileBrowser::new(Some(dir))?));
|
||||
None
|
||||
},
|
||||
_ => unreachable!(),
|
||||
})
|
||||
}
|
||||
fn filter (pool: &mut MidiPool, filter: Arc<str>) -> Perhaps<Self> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -183,15 +183,15 @@ pub const DEFAULT_CONFIGS: &'static [(&'static str, &'static str)] = &[
|
|||
|
||||
default_config!("../../../config/keys_arranger.edn"),
|
||||
default_config!("../../../config/keys_clip.edn"),
|
||||
default_config!("../../../config/keys_clip_length.edn"),
|
||||
default_config!("../../../config/keys_clip_rename.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"),
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ pub struct App {
|
|||
/// Theme
|
||||
pub color: ItemTheme,
|
||||
/// Contains all clips in the project
|
||||
pub pool: Option<MidiPool>,
|
||||
pub pool: Option<Pool>,
|
||||
/// Contains the currently edited MIDI clip
|
||||
pub editor: Option<MidiEditor>,
|
||||
/// Contains a render of the project arrangement, redrawn on update.
|
||||
|
|
@ -275,7 +275,7 @@ impl App {
|
|||
}
|
||||
|
||||
/// Get the clip pool, if present
|
||||
pub(crate) fn pool (&self) -> Option<&MidiPool> {
|
||||
pub(crate) fn pool (&self) -> Option<&Pool> {
|
||||
self.pool.as_ref()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,203 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MidiPool {
|
||||
pub visible: bool,
|
||||
/// Collection of clips
|
||||
pub clips: Arc<RwLock<Vec<Arc<RwLock<MidiClip>>>>>,
|
||||
/// Selected clip
|
||||
pub clip: AtomicUsize,
|
||||
/// Mode switch
|
||||
pub mode: Option<PoolMode>,
|
||||
}
|
||||
|
||||
impl Default for MidiPool {
|
||||
fn default () -> Self {
|
||||
use PoolMode::*;
|
||||
Self {
|
||||
visible: true,
|
||||
clips: Arc::from(RwLock::from(vec![])),
|
||||
clip: 0.into(),
|
||||
mode: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
has_clips!(|self: MidiPool|self.clips);
|
||||
|
||||
has_clip!(|self: MidiPool|self.clips().get(self.clip_index()).map(|c|c.clone()));
|
||||
|
||||
from!(|clip:&Arc<RwLock<MidiClip>>|MidiPool = {
|
||||
let model = Self::default();
|
||||
model.clips.write().unwrap().push(clip.clone());
|
||||
model.clip.store(1, Relaxed);
|
||||
model
|
||||
});
|
||||
|
||||
impl MidiPool {
|
||||
pub fn clip_index (&self) -> usize {
|
||||
self.clip.load(Relaxed)
|
||||
}
|
||||
pub fn set_clip_index (&self, value: usize) {
|
||||
self.clip.store(value, Relaxed);
|
||||
}
|
||||
pub fn mode (&self) -> &Option<PoolMode> {
|
||||
&self.mode
|
||||
}
|
||||
pub fn mode_mut (&mut self) -> &mut Option<PoolMode> {
|
||||
&mut self.mode
|
||||
}
|
||||
pub fn begin_clip_length (&mut self) {
|
||||
let length = self.clips()[self.clip_index()].read().unwrap().length;
|
||||
*self.mode_mut() = Some(PoolMode::Length(
|
||||
self.clip_index(),
|
||||
length,
|
||||
ClipLengthFocus::Bar
|
||||
));
|
||||
}
|
||||
pub fn begin_clip_rename (&mut self) {
|
||||
let name = self.clips()[self.clip_index()].read().unwrap().name.clone();
|
||||
*self.mode_mut() = Some(PoolMode::Rename(
|
||||
self.clip_index(),
|
||||
name
|
||||
));
|
||||
}
|
||||
pub fn begin_import (&mut self) -> Usually<()> {
|
||||
*self.mode_mut() = Some(PoolMode::Import(
|
||||
self.clip_index(),
|
||||
FileBrowser::new(None)?
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
pub fn begin_export (&mut self) -> Usually<()> {
|
||||
*self.mode_mut() = Some(PoolMode::Export(
|
||||
self.clip_index(),
|
||||
FileBrowser::new(None)?
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
pub fn new_clip (&self) -> MidiClip {
|
||||
MidiClip::new("Clip", true, 4 * PPQ, None, Some(ItemTheme::random()))
|
||||
}
|
||||
pub fn cloned_clip (&self) -> MidiClip {
|
||||
let index = self.clip_index();
|
||||
let mut clip = self.clips()[index].read().unwrap().duplicate();
|
||||
clip.color = ItemTheme::random_near(clip.color, 0.25);
|
||||
clip
|
||||
}
|
||||
pub fn add_new_clip (&self) -> (usize, Arc<RwLock<MidiClip>>) {
|
||||
let clip = Arc::new(RwLock::new(self.new_clip()));
|
||||
let index = {
|
||||
let mut clips = self.clips.write().unwrap();
|
||||
clips.push(clip.clone());
|
||||
clips.len().saturating_sub(1)
|
||||
};
|
||||
self.clip.store(index, Relaxed);
|
||||
(index, clip)
|
||||
}
|
||||
pub fn delete_clip (&mut self, clip: &MidiClip) -> bool {
|
||||
let index = self.clips.read().unwrap().iter().position(|x|*x.read().unwrap()==*clip);
|
||||
if let Some(index) = index {
|
||||
self.clips.write().unwrap().remove(index);
|
||||
return true
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Modes for clip pool
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum PoolMode {
|
||||
/// Renaming a pattern
|
||||
Rename(usize, Arc<str>),
|
||||
/// Editing the length of a pattern
|
||||
Length(usize, usize, ClipLengthFocus),
|
||||
/// Load clip from disk
|
||||
Import(usize, FileBrowser),
|
||||
/// Save clip to disk
|
||||
Export(usize, FileBrowser),
|
||||
}
|
||||
|
||||
/// Focused field of `ClipLength`
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum ClipLengthFocus {
|
||||
/// Editing the number of bars
|
||||
Bar,
|
||||
/// Editing the number of beats
|
||||
Beat,
|
||||
/// Editing the number of ticks
|
||||
Tick,
|
||||
}
|
||||
|
||||
impl ClipLengthFocus {
|
||||
pub fn next (&mut self) {
|
||||
use ClipLengthFocus::*;
|
||||
*self = match self { Bar => Beat, Beat => Tick, Tick => Bar, }
|
||||
}
|
||||
pub fn prev (&mut self) {
|
||||
use ClipLengthFocus::*;
|
||||
*self = match self { Bar => Tick, Beat => Bar, Tick => Beat, }
|
||||
}
|
||||
}
|
||||
|
||||
/// Displays and edits clip length.
|
||||
#[derive(Clone)]
|
||||
pub struct ClipLength {
|
||||
/// Pulses per beat (quaver)
|
||||
ppq: usize,
|
||||
/// Beats per bar
|
||||
bpb: usize,
|
||||
/// Length of clip in pulses
|
||||
pulses: usize,
|
||||
/// Selected subdivision
|
||||
pub focus: Option<ClipLengthFocus>,
|
||||
}
|
||||
|
||||
impl ClipLength {
|
||||
pub fn _new (pulses: usize, focus: Option<ClipLengthFocus>) -> Self {
|
||||
Self { ppq: PPQ, bpb: 4, pulses, focus }
|
||||
}
|
||||
pub fn bars (&self) -> usize {
|
||||
self.pulses / (self.bpb * self.ppq)
|
||||
}
|
||||
pub fn beats (&self) -> usize {
|
||||
(self.pulses % (self.bpb * self.ppq)) / self.ppq
|
||||
}
|
||||
pub fn ticks (&self) -> usize {
|
||||
self.pulses % self.ppq
|
||||
}
|
||||
pub fn bars_string (&self) -> Arc<str> {
|
||||
format!("{}", self.bars()).into()
|
||||
}
|
||||
pub fn beats_string (&self) -> Arc<str> {
|
||||
format!("{}", self.beats()).into()
|
||||
}
|
||||
pub fn ticks_string (&self) -> Arc<str> {
|
||||
format!("{:>02}", self.ticks()).into()
|
||||
}
|
||||
}
|
||||
|
||||
pub type ClipPool = Vec<Arc<RwLock<MidiClip>>>;
|
||||
|
||||
pub trait HasClips {
|
||||
fn clips <'a> (&'a self) -> std::sync::RwLockReadGuard<'a, ClipPool>;
|
||||
fn clips_mut <'a> (&'a self) -> std::sync::RwLockWriteGuard<'a, ClipPool>;
|
||||
fn add_clip (&self) -> (usize, Arc<RwLock<MidiClip>>) {
|
||||
let clip = Arc::new(RwLock::new(MidiClip::new("Clip", true, 384, None, None)));
|
||||
self.clips_mut().push(clip.clone());
|
||||
(self.clips().len() - 1, clip)
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export] macro_rules! has_clips {
|
||||
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
|
||||
impl $(<$($L),*$($T $(: $U)?),*>)? HasClips for $Struct $(<$($L),*$($T),*>)? {
|
||||
fn clips <'a> (&'a $self) -> std::sync::RwLockReadGuard<'a, ClipPool> {
|
||||
$cb.read().unwrap()
|
||||
}
|
||||
fn clips_mut <'a> (&'a $self) -> std::sync::RwLockWriteGuard<'a, ClipPool> {
|
||||
$cb.write().unwrap()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ use crate::*;
|
|||
pub(crate) use std::fmt::Write;
|
||||
pub(crate) use ::tengri::tui::ratatui::prelude::Position;
|
||||
|
||||
mod view_dialog; pub use self::view_dialog::*;
|
||||
mod view_output; pub use self::view_output::*;
|
||||
|
||||
#[tengri_proc::view(TuiOut)]
|
||||
|
|
@ -49,85 +50,11 @@ impl App {
|
|||
self.sampler().map(|s|s.view_meters_output())
|
||||
}
|
||||
pub fn view_dialog (&self) -> impl Content<TuiOut> + use<'_> {
|
||||
When::new(self.dialog.is_some(), Bsp::b(
|
||||
Fill::xy(Tui::fg_bg(Rgb(64,64,64), Rgb(32,32,32), "")),
|
||||
Fixed::xy(30, 15, 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 => self.view_dialog_help().boxed(),
|
||||
Dialog::Save => self.view_dialog_save().boxed(),
|
||||
Dialog::Load => self.view_dialog_load().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(),
|
||||
}))
|
||||
)))
|
||||
))
|
||||
view_dialog(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl App {
|
||||
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)))
|
||||
}
|
||||
fn view_dialog_help (&self) -> impl Content<TuiOut> + use<'_> {
|
||||
let bindings = ||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
|
||||
});
|
||||
//let binding = ;[> Bsp::e(
|
||||
//Fixed::x(15, Align::w(Tui::bold(true, Tui::fg(Rgb(255,192,0), if let Some(Token {
|
||||
//value: Value::Sym(key), ..
|
||||
//}) = binding.next() {
|
||||
//Some(key.to_string())
|
||||
//} else {
|
||||
//None
|
||||
//})))),
|
||||
//Bsp::e(" ", Tui::fg(Rgb(255,255,255), if let Some(Token {
|
||||
//value: Value::Key(command), ..
|
||||
//}) = binding.next() {
|
||||
//Some(command.to_string())
|
||||
//} else {
|
||||
//None
|
||||
//})),
|
||||
//);*/
|
||||
Bsp::s(Tui::bold(true, "Help"), Bsp::s("", Map::south(1, bindings, |b,i|format!("{i}:{b:?}"))))
|
||||
//|mut binding: TokenIter, _|Map::east(5, move||binding.clone(), |_,_|"kyp"))))
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
fn view_dialog_message <'a> (&'a self, message: &'a Message) -> impl Content<TuiOut> + use<'a> {
|
||||
Bsp::s(message, Bsp::s("", "[ OK ]"))
|
||||
}
|
||||
|
||||
fn view_dialog_save <'a> (&'a self) -> impl Content<TuiOut> + use<'a> {
|
||||
"WIP: SAVE"
|
||||
}
|
||||
|
||||
fn view_dialog_load <'a> (&'a self) -> impl Content<TuiOut> + use<'a> {
|
||||
"WIP: LOAD"
|
||||
}
|
||||
|
||||
fn view_dialog_options <'a> (&'a self) -> impl Content<TuiOut> + use<'a> {
|
||||
"WIP: OPTIONS"
|
||||
}
|
||||
|
||||
/// Spacing between tracks.
|
||||
pub(crate) const TRACK_SPACING: usize = 0;
|
||||
|
|
@ -752,44 +679,3 @@ impl ViewCache {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PoolView<'a>(pub bool, pub &'a MidiPool);
|
||||
|
||||
content!(TuiOut: |self: PoolView<'a>| {
|
||||
let Self(compact, model) = self;
|
||||
let MidiPool { clips, .. } = self.1;
|
||||
//let color = self.1.clip().map(|c|c.read().unwrap().color).unwrap_or_else(||Tui::g(32).into());
|
||||
let on_bg = |x|x;//Bsp::b(Repeat(" "), Tui::bg(color.darkest.rgb, x));
|
||||
let border = |x|x;//Outer(Style::default().fg(color.dark.rgb).bg(color.darkest.rgb)).enclose(x);
|
||||
let iter = | |model.clips().clone().into_iter();
|
||||
let height = clips.read().unwrap().len() as u16;
|
||||
Tui::bg(Reset, Fixed::y(height, on_bg(border(Map::new(iter, move|clip: Arc<RwLock<MidiClip>>, i|{
|
||||
let item_height = 1;
|
||||
let item_offset = i as u16 * item_height;
|
||||
let selected = i == model.clip_index();
|
||||
let MidiClip { ref name, color, length, .. } = *clip.read().unwrap();
|
||||
let bg = if selected { color.light.rgb } else { color.base.rgb };
|
||||
let fg = color.lightest.rgb;
|
||||
let name = if *compact { format!(" {i:>3}") } else { format!(" {i:>3} {name}") };
|
||||
let length = if *compact { String::default() } else { format!("{length} ") };
|
||||
Fixed::y(1, map_south(item_offset, item_height, Tui::bg(bg, lay!(
|
||||
Fill::x(Align::w(Tui::fg(fg, Tui::bold(selected, name)))),
|
||||
Fill::x(Align::e(Tui::fg(fg, Tui::bold(selected, length)))),
|
||||
Fill::x(Align::w(When::new(selected, Tui::bold(true, Tui::fg(Tui::g(255), "▶"))))),
|
||||
Fill::x(Align::e(When::new(selected, Tui::bold(true, Tui::fg(Tui::g(255), "◀"))))),
|
||||
))))
|
||||
})))))
|
||||
});
|
||||
|
||||
content!(TuiOut: |self: ClipLength| {
|
||||
use ClipLengthFocus::*;
|
||||
let bars = ||self.bars_string();
|
||||
let beats = ||self.beats_string();
|
||||
let ticks = ||self.ticks_string();
|
||||
match self.focus {
|
||||
None => row!(" ", bars(), ".", beats(), ".", ticks()),
|
||||
Some(Bar) => row!("[", bars(), "]", beats(), ".", ticks()),
|
||||
Some(Beat) => row!(" ", bars(), "[", beats(), "]", ticks()),
|
||||
Some(Tick) => row!(" ", bars(), ".", beats(), "[", ticks()),
|
||||
}
|
||||
});
|
||||
|
|
|
|||
99
crates/app/src/view/view_dialog.rs
Normal file
99
crates/app/src/view/view_dialog.rs
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
use crate::*;
|
||||
|
||||
pub(crate) fn view_dialog (app: &App) -> impl Content<TuiOut> + use<'_> {
|
||||
When::new(app.dialog.is_some(), 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(app.dialog.as_ref().map(|dialog|match dialog {
|
||||
Dialog::Menu => app.view_dialog_menu().boxed(),
|
||||
Dialog::Help => app.view_dialog_help().boxed(),
|
||||
Dialog::Save => app.view_dialog_save().boxed(),
|
||||
Dialog::Load => app.view_dialog_load().boxed(),
|
||||
Dialog::Options => app.view_dialog_options().boxed(),
|
||||
Dialog::Device(index) => app.view_dialog_device(*index).boxed(),
|
||||
Dialog::Message(message) => app.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) -> impl Content<TuiOut> + use<'a> {
|
||||
//let bindings = ;
|
||||
//let binding = ;[> Bsp::e(
|
||||
//Fixed::x(15, Align::w(Tui::bold(true, Tui::fg(Rgb(255,192,0), if let Some(Token {
|
||||
//value: Value::Sym(key), ..
|
||||
//}) = binding.next() {
|
||||
//Some(key.to_string())
|
||||
//} else {
|
||||
//None
|
||||
//})))),
|
||||
//Bsp::e(" ", Tui::fg(Rgb(255,255,255), if let Some(Token {
|
||||
//value: Value::Key(command), ..
|
||||
//}) = binding.next() {
|
||||
//Some(command.to_string())
|
||||
//} else {
|
||||
//None
|
||||
//})),
|
||||
//);*/
|
||||
Bsp::s(Tui::bold(true, "Help"), Bsp::s("", Map::south(1,
|
||||
||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
|
||||
})
|
||||
.take(20),
|
||||
|mut b,i|Bsp::e(
|
||||
Min::x(30, Max::x(60, format!("?"))),
|
||||
b.next().map(|t|Min::x(16, Tui::fg(Rgb(224,64,0), format!("{}", t.value)))),
|
||||
))))
|
||||
//format!("{b:?}")))))
|
||||
//|mut binding: TokenIter, _|Map::east(5, move||binding.clone(), |_,_|"kyp"))))
|
||||
}
|
||||
|
||||
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_save <'a> (&'a self) -> impl Content<TuiOut> + use<'a> {
|
||||
Bsp::s(
|
||||
Fill::x(Align::w(Margin::xy(1, 1, Bsp::e(
|
||||
Tui::bold(true, " Save project: "),
|
||||
Shrink::x(3, Fixed::y(1, RepeatH("🭻"))))))),
|
||||
Outer(true, Style::default().fg(Tui::g(96)))
|
||||
.enclose(Fill::xy("todo file browser")))
|
||||
}
|
||||
|
||||
pub fn view_dialog_load <'a> (&'a self) -> 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("todo file browser")))
|
||||
}
|
||||
|
||||
pub fn view_dialog_options <'a> (&'a self) -> impl Content<TuiOut> + use<'a> {
|
||||
"TODO"
|
||||
}
|
||||
}
|
||||
|
|
@ -16,13 +16,15 @@ wavers = { workspace = true, optional = true }
|
|||
winit = { workspace = true, optional = true }
|
||||
|
||||
[features]
|
||||
default = [ "clock", "editor", "sequencer", "sampler", "lv2" ]
|
||||
default = [ "browser", "clock", "editor", "sequencer", "sampler", "lv2" ]
|
||||
clock = []
|
||||
editor = []
|
||||
meter = []
|
||||
mixer = []
|
||||
sequencer = [ "clock", "uuid" ]
|
||||
sampler = [ "meter", "mixer", "symphonia", "wavers" ]
|
||||
browser = []
|
||||
pool = []
|
||||
sequencer = [ "clock", "uuid", "pool" ]
|
||||
sampler = [ "meter", "mixer", "browser", "symphonia", "wavers" ]
|
||||
lv2 = [ "livi", "winit" ]
|
||||
vst2 = []
|
||||
vst3 = []
|
||||
|
|
|
|||
3
crates/device/src/browser.rs
Normal file
3
crates/device/src/browser.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
mod browser_api; pub use self::browser_api::*;
|
||||
mod browser_model; pub use self::browser_model::*;
|
||||
mod browser_view; pub use self::browser_view::*;
|
||||
90
crates/device/src/browser/browser_api.rs
Normal file
90
crates/device/src/browser/browser_api.rs
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
use crate::*;
|
||||
|
||||
#[tengri_proc::expose]
|
||||
impl Browser {
|
||||
}
|
||||
|
||||
#[tengri_proc::command(Browser)]
|
||||
impl BrowserCommand {
|
||||
//fn set_address (browser: &mut Browser, address: PathBuf) -> Perhaps<Self> {
|
||||
//Ok(None)
|
||||
//}
|
||||
//fn set_search (browser: &mut Browser, filter: Arc<str>) -> Perhaps<Self> {
|
||||
//Ok(None)
|
||||
//}
|
||||
//fn set_cursor (browser: &mut Browser, cursor: usize) -> Perhaps<Self> {
|
||||
//Ok(None)
|
||||
//}
|
||||
}
|
||||
|
||||
// Commands supported by [Browser]
|
||||
//#[derive(Debug, Clone, PartialEq)]
|
||||
//pub enum BrowserCommand {
|
||||
//Begin,
|
||||
//Cancel,
|
||||
//Confirm,
|
||||
//Select(usize),
|
||||
//Chdir(PathBuf),
|
||||
//Filter(Arc<str>),
|
||||
//}
|
||||
//fn begin (browser: &mut Browser) -> Perhaps<Self> {
|
||||
//unreachable!();
|
||||
//}
|
||||
//fn cancel (browser: &mut Browser) -> Perhaps<Self> {
|
||||
//todo!()
|
||||
////browser.mode = None;
|
||||
////Ok(None)
|
||||
//}
|
||||
//fn confirm (browser: &mut Browser) -> Perhaps<Self> {
|
||||
//todo!()
|
||||
////Ok(match browser.mode {
|
||||
////Some(PoolMode::Import(index, ref mut browser)) => {
|
||||
////if browser.is_file() {
|
||||
////let path = browser.path();
|
||||
////browser.mode = None;
|
||||
////let _undo = PoolClipCommand::import(browser, index, path)?;
|
||||
////None
|
||||
////} else if browser.is_dir() {
|
||||
////browser.mode = Some(PoolMode::Import(index, browser.chdir()?));
|
||||
////None
|
||||
////} else {
|
||||
////None
|
||||
////}
|
||||
////},
|
||||
////Some(PoolMode::Export(index, ref mut browser)) => {
|
||||
////todo!()
|
||||
////},
|
||||
////_ => unreachable!(),
|
||||
////})
|
||||
//}
|
||||
//fn select (browser: &mut Browser, index: usize) -> Perhaps<Self> {
|
||||
//todo!()
|
||||
////Ok(match browser.mode {
|
||||
////Some(PoolMode::Import(index, ref mut browser)) => {
|
||||
////browser.index = index;
|
||||
////None
|
||||
////},
|
||||
////Some(PoolMode::Export(index, ref mut browser)) => {
|
||||
////browser.index = index;
|
||||
////None
|
||||
////},
|
||||
////_ => unreachable!(),
|
||||
////})
|
||||
//}
|
||||
//fn chdir (browser: &mut Browser, dir: PathBuf) -> Perhaps<Self> {
|
||||
//todo!()
|
||||
////Ok(match browser.mode {
|
||||
////Some(PoolMode::Import(index, ref mut browser)) => {
|
||||
////browser.mode = Some(PoolMode::Import(index, Browser::new(Some(dir))?));
|
||||
////None
|
||||
////},
|
||||
////Some(PoolMode::Export(index, ref mut browser)) => {
|
||||
////browser.mode = Some(PoolMode::Export(index, Browser::new(Some(dir))?));
|
||||
////None
|
||||
////},
|
||||
////_ => unreachable!(),
|
||||
////})
|
||||
//}
|
||||
//fn filter (browser: &mut Browser, filter: Arc<str>) -> Perhaps<Self> {
|
||||
//todo!()
|
||||
//}
|
||||
69
crates/device/src/browser/browser_model.rs
Normal file
69
crates/device/src/browser/browser_model.rs
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
use crate::*;
|
||||
|
||||
/// Browses for phrase to import/export
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Browser {
|
||||
pub cwd: PathBuf,
|
||||
pub dirs: Vec<(OsString, String)>,
|
||||
pub files: Vec<(OsString, String)>,
|
||||
pub filter: String,
|
||||
pub index: usize,
|
||||
pub scroll: usize,
|
||||
pub size: Measure<TuiOut>,
|
||||
}
|
||||
|
||||
impl Browser {
|
||||
|
||||
pub fn new (cwd: Option<PathBuf>) -> Usually<Self> {
|
||||
let cwd = if let Some(cwd) = cwd { cwd } else { std::env::current_dir()? };
|
||||
let mut dirs = vec![];
|
||||
let mut files = vec![];
|
||||
for entry in std::fs::read_dir(&cwd)? {
|
||||
let entry = entry?;
|
||||
let name = entry.file_name();
|
||||
let decoded = name.clone().into_string().unwrap_or_else(|_|"<unreadable>".to_string());
|
||||
let meta = entry.metadata()?;
|
||||
if meta.is_dir() {
|
||||
dirs.push((name, format!("📁 {decoded}")));
|
||||
} else if meta.is_file() {
|
||||
files.push((name, format!("📄 {decoded}")));
|
||||
}
|
||||
}
|
||||
Ok(Self {
|
||||
cwd,
|
||||
dirs,
|
||||
files,
|
||||
filter: "".to_string(),
|
||||
index: 0,
|
||||
scroll: 0,
|
||||
size: Measure::new(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn len (&self) -> usize {
|
||||
self.dirs.len() + self.files.len()
|
||||
}
|
||||
|
||||
pub fn is_dir (&self) -> bool {
|
||||
self.index < self.dirs.len()
|
||||
}
|
||||
|
||||
pub fn is_file (&self) -> bool {
|
||||
self.index >= self.dirs.len()
|
||||
}
|
||||
|
||||
pub fn path (&self) -> PathBuf {
|
||||
self.cwd.join(if self.is_dir() {
|
||||
&self.dirs[self.index].0
|
||||
} else if self.is_file() {
|
||||
&self.files[self.index - self.dirs.len()].0
|
||||
} else {
|
||||
unreachable!()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn chdir (&self) -> Usually<Self> {
|
||||
Self::new(Some(self.path()))
|
||||
}
|
||||
|
||||
}
|
||||
19
crates/device/src/browser/browser_view.rs
Normal file
19
crates/device/src/browser/browser_view.rs
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
use crate::*;
|
||||
|
||||
content!(TuiOut: |self: Browser| /*Stack::down(|add|{
|
||||
let mut i = 0;
|
||||
for (_, name) in self.dirs.iter() {
|
||||
if i >= self.scroll {
|
||||
add(&Tui::bold(i == self.index, name.as_str()))?;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
for (_, name) in self.files.iter() {
|
||||
if i >= self.scroll {
|
||||
add(&Tui::bold(i == self.index, name.as_str()))?;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
add(&format!("{}/{i}", self.index))?;
|
||||
Ok(())
|
||||
})*/"todo");
|
||||
|
|
@ -20,12 +20,18 @@ pub(crate) use Color::*;
|
|||
mod device;
|
||||
pub use self::device::*;
|
||||
|
||||
#[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::*;
|
||||
|
||||
|
|
|
|||
3
crates/device/src/pool.rs
Normal file
3
crates/device/src/pool.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
mod pool_api; pub use self::pool_api::*;
|
||||
mod pool_model; pub use self::pool_model::*;
|
||||
mod pool_view; pub use self::pool_view::*;
|
||||
255
crates/device/src/pool/pool_api.rs
Normal file
255
crates/device/src/pool/pool_api.rs
Normal file
|
|
@ -0,0 +1,255 @@
|
|||
use crate::*;
|
||||
|
||||
#[tengri_proc::expose]
|
||||
impl Pool {
|
||||
fn _todo_usize_ (&self) -> usize { todo!() }
|
||||
fn _todo_bool_ (&self) -> bool { todo!() }
|
||||
fn _todo_clip_ (&self) -> MidiClip { todo!() }
|
||||
fn _todo_path_ (&self) -> PathBuf { todo!() }
|
||||
fn _todo_color_ (&self) -> ItemColor { todo!() }
|
||||
fn _todo_str_ (&self) -> Arc<str> { todo!() }
|
||||
fn clip_new (&self) -> MidiClip {
|
||||
self.new_clip()
|
||||
}
|
||||
fn clip_cloned (&self) -> MidiClip {
|
||||
self.cloned_clip()
|
||||
}
|
||||
fn clip_index_current (&self) -> usize {
|
||||
0
|
||||
}
|
||||
fn clip_index_after (&self) -> usize {
|
||||
0
|
||||
}
|
||||
fn clip_index_previous (&self) -> usize {
|
||||
0
|
||||
}
|
||||
fn clip_index_next (&self) -> usize {
|
||||
0
|
||||
}
|
||||
fn color_random (&self) -> ItemColor {
|
||||
ItemColor::random()
|
||||
}
|
||||
}
|
||||
|
||||
#[tengri_proc::command(Pool)]
|
||||
impl PoolCommand {
|
||||
|
||||
/// Toggle visibility of pool
|
||||
fn show (pool: &mut Pool, visible: bool) -> Perhaps<Self> {
|
||||
pool.visible = visible;
|
||||
Ok(Some(Self::Show { visible: !visible }))
|
||||
}
|
||||
|
||||
/// Select a clip from the clip pool
|
||||
fn select (pool: &mut Pool, index: usize) -> Perhaps<Self> {
|
||||
pool.set_clip_index(index);
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Rename a clip
|
||||
fn rename (pool: &mut Pool, command: RenameCommand) -> Perhaps<Self> {
|
||||
Ok(command.delegate(pool, |command|Self::Rename{command})?)
|
||||
}
|
||||
|
||||
/// Change the length of a clip
|
||||
fn length (pool: &mut Pool, command: CropCommand) -> Perhaps<Self> {
|
||||
Ok(command.delegate(pool, |command|Self::Length{command})?)
|
||||
}
|
||||
|
||||
/// Import from file
|
||||
fn import (pool: &mut Pool, command: BrowserCommand) -> Perhaps<Self> {
|
||||
Ok(if let Some(browser) = pool.browser.as_mut() {
|
||||
command.delegate(browser, |command|Self::Import{command})?
|
||||
} else {
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
/// Export to file
|
||||
fn export (pool: &mut Pool, command: BrowserCommand) -> Perhaps<Self> {
|
||||
Ok(if let Some(browser) = pool.browser.as_mut() {
|
||||
command.delegate(browser, |command|Self::Export{command})?
|
||||
} else {
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
/// Update the contents of the clip pool
|
||||
fn clip (pool: &mut Pool, command: PoolClipCommand) -> Perhaps<Self> {
|
||||
Ok(command.execute(pool)?.map(|command|Self::Clip{command}))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl<'state> Context<'state, BrowserCommand> for Pool {
|
||||
fn get <'source> (&'state self, iter: &mut TokenIter<'source>) -> Option<BrowserCommand> {
|
||||
self.browser.as_ref().map(|p|Context::get(p, iter)).flatten()
|
||||
}
|
||||
}
|
||||
|
||||
#[tengri_proc::command(Pool)]
|
||||
impl PoolClipCommand {
|
||||
|
||||
fn add (pool: &mut Pool, index: usize, clip: MidiClip) -> Perhaps<Self> {
|
||||
let mut index = index;
|
||||
let clip = Arc::new(RwLock::new(clip));
|
||||
let mut clips = pool.clips_mut();
|
||||
if index >= clips.len() {
|
||||
index = clips.len();
|
||||
clips.push(clip)
|
||||
} else {
|
||||
clips.insert(index, clip);
|
||||
}
|
||||
Ok(Some(Self::Delete { index }))
|
||||
}
|
||||
|
||||
fn delete (pool: &mut Pool, index: usize) -> Perhaps<Self> {
|
||||
let clip = pool.clips_mut().remove(index).read().unwrap().clone();
|
||||
Ok(Some(Self::Add { index, clip }))
|
||||
}
|
||||
|
||||
fn swap (pool: &mut Pool, index: usize, other: usize) -> Perhaps<Self> {
|
||||
pool.clips_mut().swap(index, other);
|
||||
Ok(Some(Self::Swap { index, other }))
|
||||
}
|
||||
|
||||
fn import (pool: &mut Pool, index: usize, path: PathBuf) -> Perhaps<Self> {
|
||||
let bytes = std::fs::read(&path)?;
|
||||
let smf = Smf::parse(bytes.as_slice())?;
|
||||
let mut t = 0u32;
|
||||
let mut events = vec![];
|
||||
for track in smf.tracks.iter() {
|
||||
for event in track.iter() {
|
||||
t += event.delta.as_int();
|
||||
if let TrackEventKind::Midi { channel, message } = event.kind {
|
||||
events.push((t, channel.as_int(), message));
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut clip = MidiClip::new("imported", true, t as usize + 1, None, None);
|
||||
for event in events.iter() {
|
||||
clip.notes[event.0 as usize].push(event.2);
|
||||
}
|
||||
Ok(Self::Add { index, clip }.execute(pool)?)
|
||||
}
|
||||
|
||||
fn export (pool: &mut Pool, index: usize, path: PathBuf) -> Perhaps<Self> {
|
||||
todo!("export clip to midi file");
|
||||
}
|
||||
|
||||
fn set_name (pool: &mut Pool, index: usize, name: Arc<str>) -> Perhaps<Self> {
|
||||
let clip = &mut pool.clips_mut()[index];
|
||||
let old_name = clip.read().unwrap().name.clone();
|
||||
clip.write().unwrap().name = name;
|
||||
Ok(Some(Self::SetName { index, name: old_name }))
|
||||
}
|
||||
|
||||
fn set_length (pool: &mut Pool, index: usize, length: usize) -> Perhaps<Self> {
|
||||
let clip = &mut pool.clips_mut()[index];
|
||||
let old_len = clip.read().unwrap().length;
|
||||
clip.write().unwrap().length = length;
|
||||
Ok(Some(Self::SetLength { index, length: old_len }))
|
||||
}
|
||||
|
||||
fn set_color (pool: &mut Pool, index: usize, color: ItemColor) -> Perhaps<Self> {
|
||||
let mut color = ItemTheme::from(color);
|
||||
std::mem::swap(&mut color, &mut pool.clips()[index].write().unwrap().color);
|
||||
Ok(Some(Self::SetColor { index, color: color.base }))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#[tengri_proc::command(Pool)]
|
||||
impl RenameCommand {
|
||||
fn begin (pool: &mut Pool) -> Perhaps<Self> {
|
||||
unreachable!();
|
||||
}
|
||||
fn cancel (pool: &mut Pool) -> Perhaps<Self> {
|
||||
if let Some(PoolMode::Rename(clip, ref mut old_name)) = pool.mode_mut().clone() {
|
||||
pool.clips()[clip].write().unwrap().name = old_name.clone().into();
|
||||
}
|
||||
return Ok(None)
|
||||
}
|
||||
fn confirm (pool: &mut Pool) -> Perhaps<Self> {
|
||||
if let Some(PoolMode::Rename(clip, ref mut old_name)) = pool.mode_mut().clone() {
|
||||
let old_name = old_name.clone();
|
||||
*pool.mode_mut() = None;
|
||||
return Ok(Some(Self::Set { value: old_name }))
|
||||
}
|
||||
return Ok(None)
|
||||
}
|
||||
fn set (pool: &mut Pool, value: Arc<str>) -> Perhaps<Self> {
|
||||
if let Some(PoolMode::Rename(clip, ref mut old_name)) = pool.mode_mut().clone() {
|
||||
pool.clips()[clip].write().unwrap().name = value;
|
||||
}
|
||||
return Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
#[tengri_proc::command(Pool)]
|
||||
impl CropCommand {
|
||||
fn begin (pool: &mut Pool) -> Perhaps<Self> {
|
||||
unreachable!()
|
||||
}
|
||||
fn cancel (pool: &mut Pool) -> Perhaps<Self> {
|
||||
if let Some(PoolMode::Length(..)) = pool.mode_mut().clone() {
|
||||
*pool.mode_mut() = None;
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
fn set (pool: &mut Pool, length: usize) -> Perhaps<Self> {
|
||||
if let Some(PoolMode::Length(clip, ref mut length, ref mut focus))
|
||||
= pool.mode_mut().clone()
|
||||
{
|
||||
let old_length;
|
||||
{
|
||||
let clip = pool.clips()[clip].clone();//.write().unwrap();
|
||||
old_length = Some(clip.read().unwrap().length);
|
||||
clip.write().unwrap().length = *length;
|
||||
}
|
||||
*pool.mode_mut() = None;
|
||||
return Ok(old_length.map(|length|Self::Set { length }))
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
fn next (pool: &mut Pool) -> Perhaps<Self> {
|
||||
if let Some(PoolMode::Length(clip, ref mut length, ref mut focus))
|
||||
= pool.mode_mut().clone()
|
||||
{
|
||||
focus.next()
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
fn prev (pool: &mut Pool) -> Perhaps<Self> {
|
||||
if let Some(PoolMode::Length(clip, ref mut length, ref mut focus))
|
||||
= pool.mode_mut().clone()
|
||||
{
|
||||
focus.prev()
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
fn inc (pool: &mut Pool) -> Perhaps<Self> {
|
||||
if let Some(PoolMode::Length(clip, ref mut length, ref mut focus))
|
||||
= pool.mode_mut().clone()
|
||||
{
|
||||
match focus {
|
||||
ClipLengthFocus::Bar => { *length += 4 * PPQ },
|
||||
ClipLengthFocus::Beat => { *length += PPQ },
|
||||
ClipLengthFocus::Tick => { *length += 1 },
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
fn dec (pool: &mut Pool) -> Perhaps<Self> {
|
||||
if let Some(PoolMode::Length(clip, ref mut length, ref mut focus))
|
||||
= pool.mode_mut().clone()
|
||||
{
|
||||
match focus {
|
||||
ClipLengthFocus::Bar => { *length = length.saturating_sub(4 * PPQ) },
|
||||
ClipLengthFocus::Beat => { *length = length.saturating_sub(PPQ) },
|
||||
ClipLengthFocus::Tick => { *length = length.saturating_sub(1) },
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
206
crates/device/src/pool/pool_model.rs
Normal file
206
crates/device/src/pool/pool_model.rs
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
use crate::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Pool {
|
||||
pub visible: bool,
|
||||
/// Collection of clips
|
||||
pub clips: Arc<RwLock<Vec<Arc<RwLock<MidiClip>>>>>,
|
||||
/// Selected clip
|
||||
pub clip: AtomicUsize,
|
||||
/// Mode switch
|
||||
pub mode: Option<PoolMode>,
|
||||
/// Embedded file browser
|
||||
pub browser: Option<Browser>,
|
||||
}
|
||||
|
||||
impl Default for Pool {
|
||||
fn default () -> Self {
|
||||
use PoolMode::*;
|
||||
Self {
|
||||
visible: true,
|
||||
clips: Arc::from(RwLock::from(vec![])),
|
||||
clip: 0.into(),
|
||||
mode: None,
|
||||
browser: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Pool {
|
||||
pub fn clip_index (&self) -> usize {
|
||||
self.clip.load(Relaxed)
|
||||
}
|
||||
pub fn set_clip_index (&self, value: usize) {
|
||||
self.clip.store(value, Relaxed);
|
||||
}
|
||||
pub fn mode (&self) -> &Option<PoolMode> {
|
||||
&self.mode
|
||||
}
|
||||
pub fn mode_mut (&mut self) -> &mut Option<PoolMode> {
|
||||
&mut self.mode
|
||||
}
|
||||
pub fn begin_clip_length (&mut self) {
|
||||
let length = self.clips()[self.clip_index()].read().unwrap().length;
|
||||
*self.mode_mut() = Some(PoolMode::Length(
|
||||
self.clip_index(),
|
||||
length,
|
||||
ClipLengthFocus::Bar
|
||||
));
|
||||
}
|
||||
pub fn begin_clip_rename (&mut self) {
|
||||
let name = self.clips()[self.clip_index()].read().unwrap().name.clone();
|
||||
*self.mode_mut() = Some(PoolMode::Rename(
|
||||
self.clip_index(),
|
||||
name
|
||||
));
|
||||
}
|
||||
pub fn begin_import (&mut self) -> Usually<()> {
|
||||
*self.mode_mut() = Some(PoolMode::Import(
|
||||
self.clip_index(),
|
||||
Browser::new(None)?
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
pub fn begin_export (&mut self) -> Usually<()> {
|
||||
*self.mode_mut() = Some(PoolMode::Export(
|
||||
self.clip_index(),
|
||||
Browser::new(None)?
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
pub fn new_clip (&self) -> MidiClip {
|
||||
MidiClip::new("Clip", true, 4 * PPQ, None, Some(ItemTheme::random()))
|
||||
}
|
||||
pub fn cloned_clip (&self) -> MidiClip {
|
||||
let index = self.clip_index();
|
||||
let mut clip = self.clips()[index].read().unwrap().duplicate();
|
||||
clip.color = ItemTheme::random_near(clip.color, 0.25);
|
||||
clip
|
||||
}
|
||||
pub fn add_new_clip (&self) -> (usize, Arc<RwLock<MidiClip>>) {
|
||||
let clip = Arc::new(RwLock::new(self.new_clip()));
|
||||
let index = {
|
||||
let mut clips = self.clips.write().unwrap();
|
||||
clips.push(clip.clone());
|
||||
clips.len().saturating_sub(1)
|
||||
};
|
||||
self.clip.store(index, Relaxed);
|
||||
(index, clip)
|
||||
}
|
||||
pub fn delete_clip (&mut self, clip: &MidiClip) -> bool {
|
||||
let index = self.clips.read().unwrap().iter().position(|x|*x.read().unwrap()==*clip);
|
||||
if let Some(index) = index {
|
||||
self.clips.write().unwrap().remove(index);
|
||||
return true
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Modes for clip pool
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum PoolMode {
|
||||
/// Renaming a pattern
|
||||
Rename(usize, Arc<str>),
|
||||
/// Editing the length of a pattern
|
||||
Length(usize, usize, ClipLengthFocus),
|
||||
/// Load clip from disk
|
||||
Import(usize, Browser),
|
||||
/// Save clip to disk
|
||||
Export(usize, Browser),
|
||||
}
|
||||
|
||||
/// Focused field of `ClipLength`
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum ClipLengthFocus {
|
||||
/// Editing the number of bars
|
||||
Bar,
|
||||
/// Editing the number of beats
|
||||
Beat,
|
||||
/// Editing the number of ticks
|
||||
Tick,
|
||||
}
|
||||
|
||||
impl ClipLengthFocus {
|
||||
pub fn next (&mut self) {
|
||||
use ClipLengthFocus::*;
|
||||
*self = match self { Bar => Beat, Beat => Tick, Tick => Bar, }
|
||||
}
|
||||
pub fn prev (&mut self) {
|
||||
use ClipLengthFocus::*;
|
||||
*self = match self { Bar => Tick, Beat => Bar, Tick => Beat, }
|
||||
}
|
||||
}
|
||||
|
||||
/// Displays and edits clip length.
|
||||
#[derive(Clone)]
|
||||
pub struct ClipLength {
|
||||
/// Pulses per beat (quaver)
|
||||
ppq: usize,
|
||||
/// Beats per bar
|
||||
bpb: usize,
|
||||
/// Length of clip in pulses
|
||||
pulses: usize,
|
||||
/// Selected subdivision
|
||||
pub focus: Option<ClipLengthFocus>,
|
||||
}
|
||||
|
||||
impl ClipLength {
|
||||
pub fn _new (pulses: usize, focus: Option<ClipLengthFocus>) -> Self {
|
||||
Self { ppq: PPQ, bpb: 4, pulses, focus }
|
||||
}
|
||||
pub fn bars (&self) -> usize {
|
||||
self.pulses / (self.bpb * self.ppq)
|
||||
}
|
||||
pub fn beats (&self) -> usize {
|
||||
(self.pulses % (self.bpb * self.ppq)) / self.ppq
|
||||
}
|
||||
pub fn ticks (&self) -> usize {
|
||||
self.pulses % self.ppq
|
||||
}
|
||||
pub fn bars_string (&self) -> Arc<str> {
|
||||
format!("{}", self.bars()).into()
|
||||
}
|
||||
pub fn beats_string (&self) -> Arc<str> {
|
||||
format!("{}", self.beats()).into()
|
||||
}
|
||||
pub fn ticks_string (&self) -> Arc<str> {
|
||||
format!("{:>02}", self.ticks()).into()
|
||||
}
|
||||
}
|
||||
|
||||
pub type ClipPool = Vec<Arc<RwLock<MidiClip>>>;
|
||||
|
||||
pub trait HasClips {
|
||||
fn clips <'a> (&'a self) -> std::sync::RwLockReadGuard<'a, ClipPool>;
|
||||
fn clips_mut <'a> (&'a self) -> std::sync::RwLockWriteGuard<'a, ClipPool>;
|
||||
fn add_clip (&self) -> (usize, Arc<RwLock<MidiClip>>) {
|
||||
let clip = Arc::new(RwLock::new(MidiClip::new("Clip", true, 384, None, None)));
|
||||
self.clips_mut().push(clip.clone());
|
||||
(self.clips().len() - 1, clip)
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export] macro_rules! has_clips {
|
||||
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
|
||||
impl $(<$($L),*$($T $(: $U)?),*>)? HasClips for $Struct $(<$($L),*$($T),*>)? {
|
||||
fn clips <'a> (&'a $self) -> std::sync::RwLockReadGuard<'a, ClipPool> {
|
||||
$cb.read().unwrap()
|
||||
}
|
||||
fn clips_mut <'a> (&'a $self) -> std::sync::RwLockWriteGuard<'a, ClipPool> {
|
||||
$cb.write().unwrap()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
has_clips!(|self: Pool|self.clips);
|
||||
|
||||
has_clip!(|self: Pool|self.clips().get(self.clip_index()).map(|c|c.clone()));
|
||||
|
||||
from!(|clip:&Arc<RwLock<MidiClip>>|Pool = {
|
||||
let model = Self::default();
|
||||
model.clips.write().unwrap().push(clip.clone());
|
||||
model.clip.store(1, Relaxed);
|
||||
model
|
||||
});
|
||||
42
crates/device/src/pool/pool_view.rs
Normal file
42
crates/device/src/pool/pool_view.rs
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
use crate::*;
|
||||
|
||||
pub struct PoolView<'a>(pub bool, pub &'a Pool);
|
||||
|
||||
content!(TuiOut: |self: PoolView<'a>| {
|
||||
let Self(compact, model) = self;
|
||||
let Pool { clips, .. } = self.1;
|
||||
//let color = self.1.clip().map(|c|c.read().unwrap().color).unwrap_or_else(||Tui::g(32).into());
|
||||
let on_bg = |x|x;//Bsp::b(Repeat(" "), Tui::bg(color.darkest.rgb, x));
|
||||
let border = |x|x;//Outer(Style::default().fg(color.dark.rgb).bg(color.darkest.rgb)).enclose(x);
|
||||
let iter = | |model.clips().clone().into_iter();
|
||||
let height = clips.read().unwrap().len() as u16;
|
||||
Tui::bg(Reset, Fixed::y(height, on_bg(border(Map::new(iter, move|clip: Arc<RwLock<MidiClip>>, i|{
|
||||
let item_height = 1;
|
||||
let item_offset = i as u16 * item_height;
|
||||
let selected = i == model.clip_index();
|
||||
let MidiClip { ref name, color, length, .. } = *clip.read().unwrap();
|
||||
let bg = if selected { color.light.rgb } else { color.base.rgb };
|
||||
let fg = color.lightest.rgb;
|
||||
let name = if *compact { format!(" {i:>3}") } else { format!(" {i:>3} {name}") };
|
||||
let length = if *compact { String::default() } else { format!("{length} ") };
|
||||
Fixed::y(1, map_south(item_offset, item_height, Tui::bg(bg, lay!(
|
||||
Fill::x(Align::w(Tui::fg(fg, Tui::bold(selected, name)))),
|
||||
Fill::x(Align::e(Tui::fg(fg, Tui::bold(selected, length)))),
|
||||
Fill::x(Align::w(When::new(selected, Tui::bold(true, Tui::fg(Tui::g(255), "▶"))))),
|
||||
Fill::x(Align::e(When::new(selected, Tui::bold(true, Tui::fg(Tui::g(255), "◀"))))),
|
||||
))))
|
||||
})))))
|
||||
});
|
||||
|
||||
content!(TuiOut: |self: ClipLength| {
|
||||
use ClipLengthFocus::*;
|
||||
let bars = ||self.bars_string();
|
||||
let beats = ||self.beats_string();
|
||||
let ticks = ||self.ticks_string();
|
||||
match self.focus {
|
||||
None => row!(" ", bars(), ".", beats(), ".", ticks()),
|
||||
Some(Bar) => row!("[", bars(), "]", beats(), ".", ticks()),
|
||||
Some(Beat) => row!(" ", bars(), "[", beats(), "]", ticks()),
|
||||
Some(Tick) => row!(" ", bars(), ".", beats(), "[", ticks()),
|
||||
}
|
||||
});
|
||||
|
|
@ -11,7 +11,7 @@ pub(crate) use symphonia::{
|
|||
};
|
||||
|
||||
mod sampler_api; pub use self::sampler_api::*;
|
||||
mod sampler_audio; pub use self::sampler_audio::*;
|
||||
mod sampler_audio;
|
||||
mod sampler_browse; pub use self::sampler_browse::*;
|
||||
mod sampler_midi; pub use self::sampler_midi::*;
|
||||
mod sampler_model; pub use self::sampler_model::*;
|
||||
|
|
|
|||
|
|
@ -63,7 +63,8 @@ impl SamplerCommand {
|
|||
Arc::new(RwLock::new(Sample::new(
|
||||
"Sample", 0, 0, vec![vec![];sampler.audio_ins.len()]
|
||||
)))
|
||||
));
|
||||
|
||||
));
|
||||
Ok(None)
|
||||
}
|
||||
fn record_finish (sampler: &mut Sampler) -> Perhaps<Self> {
|
||||
|
|
|
|||
|
|
@ -174,5 +174,5 @@ pub struct Voice {
|
|||
#[derive(Debug)]
|
||||
pub enum SamplerMode {
|
||||
// Load sample from path
|
||||
Import(usize, FileBrowser),
|
||||
Import(usize, Browser),
|
||||
}
|
||||
|
|
|
|||
2
deps/tengri
vendored
2
deps/tengri
vendored
|
|
@ -1 +1 @@
|
|||
Subproject commit faecc2c304ad2c0ebd78d21170a02c172fd356bf
|
||||
Subproject commit b45ac8f417b2f4e83e116a9ee5fe4bf3ad57a726
|
||||
Loading…
Add table
Add a link
Reference in a new issue