mirror of
https://codeberg.org/unspeaker/tek.git
synced 2026-10-08 10:46:43 +02:00
collect tek_view, tek_ctrl
This commit is contained in:
parent
c5d6ce24b2
commit
186b81f29c
47 changed files with 2665 additions and 2893 deletions
636
src/app.rs
636
src/app.rs
|
|
@ -1,636 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
/// Total application state.
|
||||
///
|
||||
/// ```
|
||||
/// use tek::{HasTracks, HasScenes, TracksView, ScenesView};
|
||||
/// let mut app = tek::App::default();
|
||||
/// let _ = app.scenes_add_one(None, None).unwrap();
|
||||
/// let _ = app.update_clock();
|
||||
/// app.project.editor = Some(Default::default());
|
||||
/// //let _: Vec<_> = app.project.inputs_with_sizes().collect();
|
||||
/// //let _: Vec<_> = app.project.outputs_with_sizes().collect();
|
||||
/// let _: Vec<_> = app.project.tracks_with_sizes().collect();
|
||||
/// //let _: Vec<_> = app.project.scenes_with_sizes(true, 10, 10).collect();
|
||||
/// //let _: Vec<_> = app.scenes_with_colors(true, 10).collect();
|
||||
/// //let _: Vec<_> = app.scenes_with_track_colors(true, 10, 10).collect();
|
||||
/// let _ = app.project.w();
|
||||
/// //let _ = app.project.w_sidebar();
|
||||
/// //let _ = app.project.w_tracks_area();
|
||||
/// let _ = app.project.h();
|
||||
/// //let _ = app.project.h_tracks_area();
|
||||
/// //let _ = app.project.h_inputs();
|
||||
/// //let _ = app.project.h_outputs();
|
||||
/// let _ = app.project.h_scenes();
|
||||
/// ```
|
||||
#[derive(Default, Debug)]
|
||||
#[namespace(Arc<[Connect]> get_arc_array_connect)]
|
||||
#[namespace(Arc<str> get_arc_str)]
|
||||
#[namespace(Color get_color)]
|
||||
#[namespace(ControlAxis get_axis)]
|
||||
#[namespace(Dialog get_dialog)]
|
||||
#[namespace(ItemColor)]
|
||||
#[namespace(ItemTheme)]
|
||||
#[namespace(MidiClip)]
|
||||
#[namespace(Option<Arc<[Connect]>> get_opt_arc_array_connect)]
|
||||
#[namespace(Option<Arc<str>> get_opt_arc_str)]
|
||||
#[namespace(Option<ItemTheme> get_opt_itemtheme)]
|
||||
#[namespace(Option<Arc<RwLock<MidiClip>>> get_opt_arc_rwlock_midiclip)]
|
||||
#[namespace(Option<Vec<Option<Arc<RwLock<MidiClip>>>>> get_opt_vec_opt_arc_rwlock_midiclip)]
|
||||
#[namespace(Option<bool> get_opt_bool)]
|
||||
#[namespace(Option<u16> get_opt_u16)]
|
||||
#[namespace(Option<u32> get_opt_u32)]
|
||||
#[namespace(Option<usize> get_opt_usize)]
|
||||
#[namespace(PathBuf get_path_buf)]
|
||||
#[namespace(Vec<Arc<str>> get_vec_arc_str)]
|
||||
#[namespace(bool get_bool)]
|
||||
#[namespace(f64 get_f64)]
|
||||
#[namespace(isize)]
|
||||
#[namespace(u16 get_u16)]
|
||||
#[namespace(u32)]
|
||||
#[namespace(u8)]
|
||||
#[namespace(usize get_usize)]
|
||||
pub struct App {
|
||||
/// Exit flag
|
||||
pub exit: Exit,
|
||||
/// Base color.
|
||||
pub color: ItemTheme,
|
||||
/// Must not be dropped for the duration of the process
|
||||
pub jack: Jack<'static>,
|
||||
/// Display size
|
||||
pub size: Sizer,
|
||||
/// Performance counter
|
||||
pub perf: PerfModel,
|
||||
/// Available view modes and input bindings
|
||||
pub config: Arc<Config>,
|
||||
/// Currently selected mode
|
||||
pub modes: Arc<RwLock<Vec<Arc<str>>>>,
|
||||
/// Undo history
|
||||
pub history: Vec<(AppCommand, Option<AppCommand>)>,
|
||||
/// Dialog overlay
|
||||
pub dialog: Dialog,
|
||||
/// Contains all recently created clips.
|
||||
pub pool: Pool,
|
||||
/// Contains the currently edited musical arrangement
|
||||
pub project: Project,
|
||||
/// Error, if any
|
||||
pub error: Arc<RwLock<Option<Arc<str>>>>,
|
||||
#[cfg(feature = "prof2")]
|
||||
/// Tracing guard
|
||||
pub guard: Option<tracing_flame::FlushGuard<std::io::BufWriter<std::fs::File>>>,
|
||||
}
|
||||
|
||||
impl App {
|
||||
/// Create a new application instance from a backend, project, config, and mode
|
||||
///
|
||||
/// ```
|
||||
/// let mut proj = tek::Project::default();
|
||||
/// proj.jack = tek::tengri::Jack::new(&"test_tek").expect("failed to connect to jack");
|
||||
/// let mut conf = tek::Config::load(tek::Config::default(), "(mode hello)").unwrap();
|
||||
/// let tek = tek::App::new(None, proj, conf.into(), "hello");
|
||||
/// ```
|
||||
pub fn new (
|
||||
exit: Option<Exit>,
|
||||
project: Project,
|
||||
config: Arc<Config>,
|
||||
mode: impl AsRef<str>
|
||||
) -> Self {
|
||||
App {
|
||||
exit: exit.unwrap_or_default(),
|
||||
jack: project.jack.clone(),
|
||||
color: ItemTheme::random(),
|
||||
dialog: Dialog::welcome(),
|
||||
modes: Arc::new(RwLock::new(vec![mode.as_ref().into()])),
|
||||
config,
|
||||
project,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_arc_str <'a> (_: &App, src: impl Language) -> Perhaps<Arc<str>> {
|
||||
Ok(src.src()?.map(|x|x.into()))
|
||||
}
|
||||
|
||||
fn get_u16 <'a> (state: &App, src: impl Language) -> Perhaps<u16> {
|
||||
Ok(Some(match src.word()? {
|
||||
Some(":w/sidebar") => state.project.w_sidebar(state.editor().is_some()),
|
||||
Some(":h/sample-detail") => 6.max(state.size.h() as u16 * 3 / 9),
|
||||
_ => return try_to_u16(src)
|
||||
}))
|
||||
}
|
||||
|
||||
fn get_usize <'a> (state: &App, src: impl Language) -> Perhaps<usize> {
|
||||
Ok(Some(match src.word()? {
|
||||
Some(":scene-count") => state.scenes().len(),
|
||||
Some(":track-count") => state.tracks().len(),
|
||||
Some(":device-kind") => state.dialog.device_kind().unwrap_or(0),
|
||||
Some(":device-kind/next") => state.dialog.device_kind_next().unwrap_or(0),
|
||||
Some(":device-kind/prev") => state.dialog.device_kind_prev().unwrap_or(0),
|
||||
_ => return try_to_usize(src)
|
||||
}))
|
||||
}
|
||||
|
||||
fn get_f64 <'a> (_state: &App, _src: impl Language) -> Perhaps<f64> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn get_bool <'a> (state: &App, src: impl Language) -> Perhaps<bool> {
|
||||
src.word()?.map(|word|Ok(match word {
|
||||
"Y" => true,
|
||||
"N" => false,
|
||||
":mode/editor" => state.project.editor.is_some(),
|
||||
":focused/dialog" => !matches!(state.dialog, Dialog::None),
|
||||
":focused/message" => matches!(state.dialog, Dialog::Message(..)),
|
||||
":focused/add_device" => matches!(state.dialog, Dialog::Device(..)),
|
||||
":focused/browser" => state.dialog.browser().is_some(),
|
||||
":focused/pool/import" => matches!(state.pool.mode, Some(PoolMode::Import(..))),
|
||||
":focused/pool/export" => matches!(state.pool.mode, Some(PoolMode::Export(..))),
|
||||
":focused/pool/rename" => matches!(state.pool.mode, Some(PoolMode::Rename(..))),
|
||||
":focused/pool/length" => matches!(state.pool.mode, Some(PoolMode::Length(..))),
|
||||
":focused/clip" => !state.editor_focused() && matches!(state.selection(), Selection::TrackClip{..}),
|
||||
":focused/track" => !state.editor_focused() && matches!(state.selection(), Selection::Track(..)),
|
||||
":focused/scene" => !state.editor_focused() && matches!(state.selection(), Selection::Scene(..)),
|
||||
":focused/mix" => !state.editor_focused() && matches!(state.selection(), Selection::Mix),
|
||||
_ => return err!("not bool: {word}")
|
||||
})).transpose()
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
fn get_selection <'a> (state: &App, src: impl Language) -> Perhaps<Selection> {
|
||||
src.word()?.map(|word|Ok(match word {
|
||||
":select/scene" => state.selection().select_scene(state.tracks().len()),
|
||||
":select/scene/next" => state.selection().select_scene_next(state.scenes().len()),
|
||||
":select/scene/prev" => state.selection().select_scene_prev(),
|
||||
":select/track" => state.selection().select_track(state.tracks().len()),
|
||||
":select/track/next" => state.selection().select_track_next(state.tracks().len()),
|
||||
":select/track/prev" => state.selection().select_track_prev(),
|
||||
_ => return err!("not selection: {word}")
|
||||
})).transpose()
|
||||
}
|
||||
|
||||
fn get_color <'a> (_: &App, src: impl Language) -> Perhaps<Color> {
|
||||
if let Some(expr) = src.expr()? {
|
||||
match (expr.head()?, expr.tail()?) {
|
||||
(Some("g"), Some(tail)) => {
|
||||
let n = try_to_u8(tail.head().map_err(Into::into))?
|
||||
.ok_or(LanguageError::domain("not gray"))?;
|
||||
Ok(Some(Color::Rgb(n, n, n)))
|
||||
},
|
||||
(Some("rgb"), Some(tail)) => {
|
||||
let r = try_to_u8(tail.head().map_err(Into::into))?
|
||||
.ok_or(LanguageError::domain("not red"))?;
|
||||
let g = try_to_u8(tail.tail().head().map_err(Into::into))?
|
||||
.ok_or(LanguageError::domain("not green"))?;
|
||||
let b = try_to_u8(tail.tail().tail().head().map_err(Into::into))?
|
||||
.ok_or(LanguageError::domain("not blue"))?;
|
||||
Ok(Some(Color::Rgb(r, g, b)))
|
||||
},
|
||||
(Some(_), _) => return err!("not a color expression: {expr}"),
|
||||
(None, _) => return err!("not a color expression: {expr}"),
|
||||
}
|
||||
} else if let Ok(Some(sym)) = src.word() {
|
||||
Ok(match sym {
|
||||
":color/bg" => Some(Color::Rgb(28, 32, 36)),
|
||||
":color/fg" => Some(Color::Rgb(98, 92, 96)),
|
||||
_ => return err!("not a color: {sym}")
|
||||
})
|
||||
} else {
|
||||
return err!("not a color: {:?}", src.src()?)
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
fn get_opt_u7 <'a> (state: &App, src: impl Language) -> Perhaps<Option<u7>> {
|
||||
eval_word(src, |word|Ok(match word {
|
||||
|
||||
":editor/pitch" => Some((
|
||||
state.editor().as_ref().map(|e|e.get_note_pos()).unwrap() as u8
|
||||
).into()),
|
||||
|
||||
_ => return err!("unknown midi note: {word}")
|
||||
|
||||
}))
|
||||
}
|
||||
|
||||
fn get_opt_usize <'a> (state: &App, src: impl Language) -> Perhaps<Option<usize>> {
|
||||
eval_word(src, |word|Ok(match word {
|
||||
|
||||
":selected/scene" => state.selection().scene(),
|
||||
":selected/track" => state.selection().track(),
|
||||
|
||||
_ => return err!("unknown opt<usize>: {word}")
|
||||
|
||||
}))
|
||||
}
|
||||
|
||||
fn get_axis <'a> (_: &App, src: impl Language) -> Perhaps<ControlAxis> {
|
||||
eval_word(src, |word|Ok(match word {
|
||||
|
||||
"X" => ControlAxis::X, "XFine" | "X2" => ControlAxis::XFine,
|
||||
"Y" => ControlAxis::Y, "YFine" | "Y2" => ControlAxis::YFine,
|
||||
"Z" => ControlAxis::Z, "ZFine" | "Z2" => ControlAxis::ZFine,
|
||||
|
||||
_ => return err!("unknown axis {word}")
|
||||
|
||||
}))
|
||||
}
|
||||
|
||||
fn get_opt_u16 <'a> (_: &App, _src: impl Language) -> Perhaps<Option<u16>> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn get_opt_u32 <'a> (_: &App, _src: impl Language) -> Perhaps<Option<u32>> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn get_opt_itemtheme <'a> (_: &App, _: impl Language) -> Perhaps<Option<ItemTheme>> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn get_opt_arc_array_connect <'a> (_: &App, _: impl Language) -> Perhaps<Option<Arc<[Connect]>>> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn get_arc_array_connect <'a> (_: &App, _: impl Language) -> Perhaps<Arc<[Connect]>> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn get_opt_arc_str <'a> (_: &App, _: impl Language) -> Perhaps<Option<Arc<str>>> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn get_opt_bool <'a> (_: &App, _: impl Language) -> Perhaps<Option<bool>> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn get_path_buf <'a> (_: &App, _: impl Language) -> Perhaps<PathBuf> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn get_vec_arc_str <'a> (_: &App, _: impl Language) -> Perhaps<Vec<Arc<str>>> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
impl_has!(Clock: |self: App|self.project.clock);
|
||||
impl_has!(Dialog: |self: App|self.dialog);
|
||||
impl_has!(Jack<'static>: |self: App|self.jack);
|
||||
impl_has!(Pool: |self: App|self.pool);
|
||||
impl_has!(Selection: |self: App|self.project.selection);
|
||||
impl_as_ref!(Option<MidiEditor>: |self: App|self.project.as_ref());
|
||||
impl_as_mut!(Option<MidiEditor>: |self: App|self.project.as_mut());
|
||||
|
||||
primitive!(u8: try_to_u8);
|
||||
primitive!(u16: try_to_u16);
|
||||
primitive!(usize: try_to_usize);
|
||||
primitive!(isize: try_to_isize);
|
||||
|
||||
impl HasClips for App {
|
||||
fn clip (&self) -> Option<Arc<RwLock<MidiClip>>> {
|
||||
self.project.clip()
|
||||
}
|
||||
fn clip_mut (&self) -> &mut Option<Arc<RwLock<MidiClip>>> {
|
||||
self.project.clip_mut()
|
||||
}
|
||||
fn clips <'a> (&'a self) -> RwLockReadGuard<'a, Clips> {
|
||||
self.project.clips()
|
||||
}
|
||||
fn clips_mut <'a> (&'a self) -> RwLockWriteGuard<'a, Clips> {
|
||||
self.project.clips_mut()
|
||||
}
|
||||
}
|
||||
|
||||
tui_keys!(self: App, input {
|
||||
#[cfg(feature = "prof2")] profiling::scope!("App::tui_keys!");
|
||||
Self::input_command(self, input)
|
||||
});
|
||||
|
||||
impl App {
|
||||
|
||||
fn input_command (state: &mut Self, input: &TuiEvent) -> Usually<()> {
|
||||
if let Some(bindings) = Self::input_command_find(state, input) {
|
||||
return Self::input_command_exec(state, bindings)
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn input_command_find (state: &mut Self, input: &TuiEvent) -> Option<Arc<[Binding<Arc<str>>]>> {
|
||||
for name in state.modes.read().iter().rev() {
|
||||
let mode = state.config.get_mode(name).as_ref().map(Arc::clone);
|
||||
if let Some(mode) = mode {
|
||||
let binds = state.config.binds.clone();
|
||||
for id in mode.keys.iter() {
|
||||
if let Some(event_map) = binds.find(id)
|
||||
&& let Some(bindings) = event_map.query(input) {
|
||||
return Some(bindings.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn input_command_exec (state: &mut Self, bindings: Arc<[Binding<Arc<str>>]>) -> Usually<()> {
|
||||
for binding in bindings.iter() {
|
||||
for command in binding.commands.iter() {
|
||||
if Self::command::<AppCommand>(state, command)? {
|
||||
return Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn command <
|
||||
T: Dispatch<Self> + Clone + Into<AppCommand>
|
||||
> (state: &mut Self, src: &str) -> Usually<bool> where
|
||||
Self: Namespace<T>,
|
||||
{
|
||||
if let Some(command) = Namespace::<T>::namespace(state, &src)? {
|
||||
return Self::command_dispatch(state, command.into())
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
fn command_dispatch (state: &mut Self, command: AppCommand) -> Usually<bool> where
|
||||
Self: Namespace<AppCommand>,
|
||||
{
|
||||
match command.clone().dispatch(state) {
|
||||
Err(err) => {
|
||||
state.history.push((command.into(), None));
|
||||
return Err(err)
|
||||
},
|
||||
Ok(undo) => {
|
||||
state.history.push((command.into(), undo.map(Into::into)));
|
||||
return Ok(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tek_proc::commands(AppCommand)]
|
||||
impl App {
|
||||
|
||||
/// Exit the application.
|
||||
#[command(Exit = "exit")]
|
||||
pub fn command_exit (&mut self) -> Perhaps<AppCommand> {
|
||||
self.exit.exit();
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Cancel current selection.
|
||||
#[command(Cancel = "cancel")]
|
||||
pub fn command_cancel (&mut self) -> Perhaps<AppCommand> {
|
||||
todo!("cancel")
|
||||
}
|
||||
|
||||
/// Confirm current selection.
|
||||
#[command(Confirm = "confirm")]
|
||||
pub fn command_confirm (&mut self) -> Perhaps<AppCommand> {
|
||||
Ok(match self.dialog.clone() {
|
||||
Dialog::Menu(index, items) => {
|
||||
for command in items[index].1.iter() {
|
||||
App::command_dispatch(self, command.clone())?;
|
||||
}
|
||||
None
|
||||
}
|
||||
_ => todo!(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Swap currently active modal dialog.
|
||||
///
|
||||
/// ```
|
||||
/// let _ = tek::App::default().command_dialog(tek::Dialog::welcome());
|
||||
/// ```
|
||||
#[command(Dialog = "dialog")]
|
||||
pub fn command_dialog (&mut self, dialog: Dialog) -> Perhaps<AppCommand> {
|
||||
let mut dialog = dialog.clone();
|
||||
std::mem::swap(&mut self.dialog, &mut dialog);
|
||||
Ok(Some(AppCommand::Dialog { dialog }))
|
||||
}
|
||||
|
||||
#[command(Mode = "mode")]
|
||||
pub fn command_mode (&mut self, command: ModeCommand) -> Perhaps<AppCommand> {
|
||||
Ok(command.dispatch(self)?.map(Into::into))
|
||||
}
|
||||
|
||||
#[command(Project = "session")]
|
||||
pub fn command_session (&mut self, command: ProjectCommand) -> Perhaps<AppCommand> {
|
||||
Ok(command.dispatch(self)?.map(Into::into))
|
||||
}
|
||||
|
||||
#[command(Axis = "axis")]
|
||||
pub fn command_axis (&mut self, command: AxisCommand) -> Perhaps<AppCommand> {
|
||||
Ok(command.dispatch(self)?.map(Into::into))
|
||||
}
|
||||
|
||||
#[command(Browse = "browse")]
|
||||
pub fn command_browse (&mut self, command: BrowseCommand) -> Perhaps<AppCommand> {
|
||||
Ok(command.dispatch(self)?.map(Into::into))
|
||||
}
|
||||
|
||||
#[command(MidiClip = "clip")]
|
||||
pub fn command_midi_clip (&mut self, command: MidiClipCommand) -> Perhaps<AppCommand> {
|
||||
Ok(command.dispatch(self)?.map(Into::into))
|
||||
}
|
||||
|
||||
#[command(MidiEdit = "edit")]
|
||||
pub fn command_midi_edit (&mut self, command: MidiEditCommand) -> Perhaps<AppCommand> {
|
||||
Ok(command.dispatch(self)?.map(Into::into))
|
||||
}
|
||||
|
||||
#[command(Pool = "pool")]
|
||||
pub fn command_pool (&mut self, command: PoolCommand) -> Perhaps<AppCommand> {
|
||||
Ok(command.dispatch(self)?.map(Into::into))
|
||||
}
|
||||
|
||||
#[command(Scene = "scene")]
|
||||
pub fn command_scene (&mut self, command: SceneCommand) -> Perhaps<AppCommand> {
|
||||
Ok(command.dispatch(self)?.map(Into::into))
|
||||
}
|
||||
|
||||
#[command(Scenes = "scenes")]
|
||||
pub fn command_scenes (&mut self, command: ScenesCommand) -> Perhaps<AppCommand> {
|
||||
Ok(command.dispatch(self)?.map(Into::into))
|
||||
}
|
||||
|
||||
#[command(Track = "track")]
|
||||
pub fn command_track (&mut self, command: TrackCommand) -> Perhaps<AppCommand> {
|
||||
Ok(command.dispatch(self)?.map(Into::into))
|
||||
}
|
||||
|
||||
#[command(Tracks = "tracks")]
|
||||
pub fn command_tracks (&mut self, command: TracksCommand) -> Perhaps<AppCommand> {
|
||||
Ok(command.dispatch(self)?.map(Into::into))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// A control axis.
|
||||
///
|
||||
/// ```
|
||||
/// let axis = tek::ControlAxis::X;
|
||||
/// ```
|
||||
#[derive(Debug, Copy, Clone)] pub enum ControlAxis {
|
||||
X, XFine,
|
||||
Y, YFine,
|
||||
Z, ZFine,
|
||||
I, IFine,
|
||||
}
|
||||
|
||||
#[tek_proc::commands(AxisCommand)]
|
||||
impl App {
|
||||
/// Increment a given data axis.
|
||||
#[command(Inc = "inc")]
|
||||
pub fn inc (&mut self, axis: ControlAxis) -> Perhaps<AxisCommand> {
|
||||
Ok(match (&self.dialog, axis) {
|
||||
(Dialog::None, _) => todo!(),
|
||||
(Dialog::Menu(_, _), ControlAxis::Y) => {
|
||||
AppCommand::Dialog { dialog: self.dialog.menu_next() }.dispatch(self)?;
|
||||
None
|
||||
},
|
||||
_ => todo!()
|
||||
})
|
||||
}
|
||||
/// Decrement a given data axis.
|
||||
#[command(Dec = "dec")]
|
||||
pub fn dec (&mut self, axis: ControlAxis) -> Perhaps<AxisCommand> {
|
||||
Ok(match (&self.dialog, axis) {
|
||||
(Dialog::None, _) => None,
|
||||
(Dialog::Menu(_, _), ControlAxis::Y) => {
|
||||
AppCommand::Dialog { dialog: self.dialog.menu_prev() }.dispatch(self)?;
|
||||
None
|
||||
},
|
||||
_ => todo!()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl_from!(AppCommand: |x: AxisCommand| AppCommand::Axis { command: x });
|
||||
impl_from!(AppCommand: |x: BrowseCommand| AppCommand::Browse { command: x });
|
||||
impl_from!(AppCommand: |x: MidiClipCommand| AppCommand::MidiClip { command: x });
|
||||
impl_from!(AppCommand: |x: MidiEditCommand| AppCommand::MidiEdit { command: x });
|
||||
impl_from!(AppCommand: |x: ModeCommand| AppCommand::Mode { command: x });
|
||||
impl_from!(AppCommand: |x: PoolCommand| AppCommand::Pool { command: x });
|
||||
impl_from!(AppCommand: |x: ProjectCommand| AppCommand::Project { command: x });
|
||||
impl_from!(AppCommand: |x: SceneCommand| AppCommand::Scene { command: x });
|
||||
impl_from!(AppCommand: |x: ScenesCommand| AppCommand::Scenes { command: x });
|
||||
impl_from!(AppCommand: |x: TrackCommand| AppCommand::Track { command: x });
|
||||
impl_from!(AppCommand: |x: TracksCommand| AppCommand::Tracks { command: x });
|
||||
|
||||
/// The [Draw] implementation for [App] handles the loaded view,
|
||||
/// which is defined in terms of [dizzle] DSL.
|
||||
///
|
||||
/// If there is an error, the error is displayed. FIXME: overlay it
|
||||
/// Then, every top-level form of the DSL description is rendered.
|
||||
impl Draw<Tui> for App {
|
||||
fn draw (&self, to: &mut Tui) -> Drawn<u16> {
|
||||
#[cfg(feature = "prof2")] profiling::scope!("App::draw");
|
||||
//self.perf.cycle(&mut |_|{
|
||||
self.draw_error(to)?;
|
||||
self.draw_modes(to)?;
|
||||
self.draw_debug(to)?;
|
||||
#[cfg(feature = "prof2")] profiling::finish_frame!();
|
||||
Ok(Some(to.area().into()))
|
||||
//})
|
||||
}
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn draw_error (&self, to: &mut Tui) -> Usually<()> {
|
||||
if let Some(e) = self.error.try_read().unwrap().as_ref() {
|
||||
e.as_ref().align_c().draw(to)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw_modes (&self, to: &mut Tui) -> Usually<()> {
|
||||
for name in self.modes.read().iter() {
|
||||
let mut error = false;
|
||||
if let Some(mode) = self.config.get_mode(name).as_ref().map(Arc::clone) {
|
||||
for (index, view) in mode.view.iter().enumerate() {
|
||||
match (view.render)(self, to) {
|
||||
Ok(None) => {},
|
||||
Ok(Some(XYWH(.., w, h))) => {
|
||||
self.size.0.store(w as usize, Relaxed);
|
||||
self.size.1.store(h as usize, Relaxed);
|
||||
},
|
||||
Err(e) => {
|
||||
let message = format!(
|
||||
"Mode: {:?}\n\nLayer: #{index}\n\nError: {e}\n\nSource:\n{}",
|
||||
&mode.name,
|
||||
&view.source
|
||||
);
|
||||
*self.error.try_write().unwrap() = Some(message.into());
|
||||
error = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if error {
|
||||
break
|
||||
} else {
|
||||
*self.error.try_write().unwrap() = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(unused)] fn draw_debug (&self, to: &mut Tui) -> Drawn<u16> {
|
||||
Split::Above.bar(0,
|
||||
Split::West.bar(16,
|
||||
format!("{}/{} {} ",
|
||||
self.perf.used.load(Relaxed),
|
||||
self.perf.window.load(Relaxed),
|
||||
self.perf.clock.raw() / 10000000).align_e(),
|
||||
format!("{}x{} ",
|
||||
self.size.0.load(Relaxed),
|
||||
self.size.1.load(Relaxed)).align_w(),
|
||||
).align_se(),
|
||||
Split::East.bar(16,
|
||||
format!("{}", self.modes.read()[0]),
|
||||
format!("{}", self.modes.read()[0])
|
||||
).align_sw()
|
||||
).draw(to)
|
||||
}
|
||||
}
|
||||
|
||||
impl_has!(Sizer: |self: App|self.size);
|
||||
|
||||
pub trait HasWidth {
|
||||
const MIN_WIDTH: usize;
|
||||
/// Increment track width.
|
||||
fn width_inc (&mut self);
|
||||
/// Decrement track width, down to a hardcoded minimum of [Self::MIN_WIDTH].
|
||||
fn width_dec (&mut self);
|
||||
}
|
||||
|
||||
pub fn view_templates <'a> (state: &'a App) -> impl Draw<Tui> + use<'a> {
|
||||
let height = (state.config.modes.len() * 2) as u16;
|
||||
draw(move |to: &mut Tui|{
|
||||
let mut index = 0;
|
||||
state.config.modes.for_each(&mut |id: &str, profile: &Mode| {
|
||||
let b = if index == 0 { Rgb(70,70,70) } else { Rgb(50,50,50) };
|
||||
let name = profile.name.get(0).map(|x|x.as_ref()).unwrap_or("<no name>");
|
||||
let info = profile.info.get(0).map(|x|x.as_ref()).unwrap_or("<no info>");
|
||||
let fg1 = Rgb(224, 192, 128);
|
||||
let fg2 = Rgb(224, 128, 32);
|
||||
let field_name = fg(fg1, name).align_w().full_w();
|
||||
let field_id = fg(fg2, id).align_e().full_w();
|
||||
let field_info = info.align_w().full_w();
|
||||
let _ = bg(b, south(above(field_name, field_id), field_info))
|
||||
.full_w().exact_h(2).push_y((2 * index) as u16).draw(to);
|
||||
index += 1;
|
||||
});
|
||||
Ok(Some(to.area().into()))
|
||||
}).min_w(30).exact_h(height)
|
||||
}
|
||||
|
||||
pub fn draw_status_bar (to: &mut Tui, state: &App) -> Drawn<u16> {
|
||||
bar(Split::South, 1u16,
|
||||
dim(true, "Mode"),
|
||||
bold(true, state.modes.read().get(0))
|
||||
).push_x(1).draw(to)
|
||||
}
|
||||
|
|
@ -1,78 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
/// Contains memoized renders of clock values.
|
||||
///
|
||||
/// Performance optimization.
|
||||
#[derive(Debug)] pub struct ClockView {
|
||||
pub sr: Memo<Option<(bool, f64)>, String>,
|
||||
pub buf: Memo<Option<f64>, String>,
|
||||
pub lat: Memo<Option<f64>, String>,
|
||||
pub bpm: Memo<Option<f64>, String>,
|
||||
pub beat: Memo<Option<f64>, String>,
|
||||
pub time: Memo<Option<f64>, String>,
|
||||
}
|
||||
|
||||
impl_default!(ClockView: {
|
||||
let mut beat = String::with_capacity(16);
|
||||
let _ = write!(beat, "{}", Self::BEAT_EMPTY);
|
||||
let mut time = String::with_capacity(16);
|
||||
let _ = write!(time, "{}", Self::TIME_EMPTY);
|
||||
let mut bpm = String::with_capacity(16);
|
||||
let _ = write!(bpm, "{}", Self::BPM_EMPTY);
|
||||
Self {
|
||||
beat: Memo::new(None, beat),
|
||||
time: Memo::new(None, time),
|
||||
bpm: Memo::new(None, bpm),
|
||||
sr: Memo::new(None, String::with_capacity(16)),
|
||||
buf: Memo::new(None, String::with_capacity(16)),
|
||||
lat: Memo::new(None, String::with_capacity(16)),
|
||||
}
|
||||
});
|
||||
|
||||
impl ClockView {
|
||||
pub const BEAT_EMPTY: &'static str = "-.-.--";
|
||||
pub const TIME_EMPTY: &'static str = "-.---s";
|
||||
pub const BPM_EMPTY: &'static str = "---.---";
|
||||
pub fn update_clock (cache: &Arc<RwLock<Self>>, clock: &Clock, compact: bool) {
|
||||
let rate = clock.timebase.sr.get();
|
||||
let chunk = clock.chunk.load(Relaxed) as f64;
|
||||
let lat = chunk / rate * 1000.;
|
||||
let delta = |start: &Moment|clock.global.usec.get() - start.usec.get();
|
||||
let mut cache = cache.try_write().unwrap();
|
||||
|
||||
cache.buf.update(
|
||||
Some(chunk), rewrite!(buf, "{chunk}")
|
||||
);
|
||||
|
||||
cache.lat.update(
|
||||
Some(lat), rewrite!(buf, "{lat:.1}ms")
|
||||
);
|
||||
|
||||
cache.sr.update(
|
||||
Some((compact, rate)), |buf: &mut String,_,_|{
|
||||
buf.clear();
|
||||
if compact {
|
||||
write!(buf, "{:.1}kHz", rate / 1000.)
|
||||
} else {
|
||||
write!(buf, "{:.0}Hz", rate)
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if let Some(now) = clock.started.try_read().unwrap().as_ref().map(delta) {
|
||||
let pulse = clock.timebase.usecs_to_pulse(now);
|
||||
let time = now/1000000.;
|
||||
let bpm = clock.timebase.bpm.get();
|
||||
cache.beat.update(Some(pulse), |buf, _, _|{
|
||||
buf.clear();
|
||||
clock.timebase.format_beats_1_to(buf, pulse)
|
||||
});
|
||||
cache.time.update(Some(time), rewrite!(buf, "{:.3}s", time));
|
||||
cache.bpm.update(Some(bpm), rewrite!(buf, "{:.3}", bpm));
|
||||
} else {
|
||||
cache.beat.update(None, rewrite!(buf, "{}", ClockView::BEAT_EMPTY));
|
||||
cache.time.update(None, rewrite!(buf, "{}", ClockView::TIME_EMPTY));
|
||||
cache.bpm.update(None, rewrite!(buf, "{}", ClockView::BPM_EMPTY));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -7,13 +7,10 @@ use notify_debouncer_full::notify::{RecursiveMode, PollWatcher, Config as Notify
|
|||
/// ```
|
||||
/// use ::{std::sync::Arc, tek::{Config, Modes, Mode}};
|
||||
/// let config: Config = Default::default();
|
||||
/// Modes::add(&config.modes, stringify!(
|
||||
/// (mode :menu (name Menu)
|
||||
/// (info Mode selector.) (keys :axis/y :confirm)
|
||||
/// (view (bg (g 0) (bsp/s :ports/out
|
||||
/// (bsp/n :ports/in
|
||||
/// (bg (g 30) (bsp/s (fixed/y 7 :logo)
|
||||
/// (fill :dialog/menu)))))))))).unwrap();
|
||||
/// let mode = stringify!(mode :menu (name Menu) (info Mode selector.) (keys Y Confirm)
|
||||
/// (view (bg (g 0) (bsp/s PortsOut (bsp/n PortsIn
|
||||
/// (bg (g 30) (bsp/s (exact/y 7 Logo) (fill Dialog))))))));
|
||||
/// Modes::add(&config.modes, mode).unwrap();
|
||||
/// let mode: Arc<Mode> = config.get_mode(":menu").unwrap();
|
||||
/// ```
|
||||
#[derive(Default, Debug)]
|
||||
|
|
@ -60,7 +57,7 @@ impl Config {
|
|||
if let Some(expr) = src.expr()? {
|
||||
match expr.head()? {
|
||||
Some("mode") => Modes::add(&config.as_ref().modes, expr.tail()),
|
||||
Some("keys") => Binds::add(&config.as_ref().binds, expr.tail()),
|
||||
Some("keys") => config.as_ref().binds.add(expr.tail()),
|
||||
Some("view") => Views::add(&config.as_ref().views, expr.tail()),
|
||||
_ => return Err(format!("Config::load: expected view/keys/mode expr, got: {src:?}").into())
|
||||
}?;
|
||||
|
|
|
|||
|
|
@ -8,49 +8,13 @@ pub struct Binds(pub Arc<RwLock<BTreeMap<
|
|||
>>>);
|
||||
|
||||
impl Binds {
|
||||
pub fn add <'a> (binds: &Binds, expr: impl Language) -> UsuallyRef<'a, ()> {
|
||||
pub fn add <'a> (&self, expr: impl Language) -> UsuallyRef<'a, ()> {
|
||||
let name = expr.head()?.ok_or("bind: missing name")?;
|
||||
let body = expr.tail()?.unwrap_or("");
|
||||
binds.0.try_write().unwrap().insert(name.into(), {
|
||||
let mut map: Bind<TuiEvent, Arc<str>> = Bind::new();
|
||||
body.each((), |_, item: &str|if matches!(item.expr().head(), Ok(Some("see"))) {
|
||||
Binds::add_ref(&mut map, item)
|
||||
} else if let Ok(Some(_word)) = item.expr().head().word() {
|
||||
Binds::add_def(&mut map, item)
|
||||
} else {
|
||||
return err!("Binds::add: unexpected: {item:?}")
|
||||
})?;
|
||||
map.into()
|
||||
});
|
||||
self.0.try_write().unwrap().insert(name.into(), Bind::load(body)?.into());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn add_ref (_map: &mut Bind<TuiEvent, Arc<str>>, _item: &str) -> Usually<()> {
|
||||
// TODO
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn add_def (map: &mut Bind<TuiEvent, Arc<str>>, item: &str) -> Usually<()> {
|
||||
let expr = item.expr()?;
|
||||
let head = expr.head()?;
|
||||
if let Some(event) = TuiKey::from_dsl(&head)?.to_crossterm() {
|
||||
let mut commands: Vec<Arc<str>> = vec![];
|
||||
item.expr()?.tail()?.each((), |_, def|Ok(commands.push(def.into())))?;
|
||||
map.add(TuiEvent(event), Binding {
|
||||
commands: commands.into(),
|
||||
condition: None,
|
||||
description: None,
|
||||
source: None
|
||||
});
|
||||
Ok(())
|
||||
} else if Some(":char") == item.expr()?.head()? {
|
||||
// TODO
|
||||
Ok(())
|
||||
} else {
|
||||
err!("Binds::add: invalid key: {:?}", item.expr()?.head()?)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear (&self) {
|
||||
*self.0.write() = Default::default();
|
||||
}
|
||||
|
|
@ -86,7 +50,7 @@ pub struct Bind<E, C>(
|
|||
pub BTreeMap<E, Vec<Binding<C>>>
|
||||
);
|
||||
|
||||
/// A sequence of zero or more commands (e.g. [AppCommand]),
|
||||
/// A sequence of zero or more commands (e.g. [AppCmd]),
|
||||
/// optionally filtered by [Condition] to form layers.
|
||||
///
|
||||
/// ```
|
||||
|
|
@ -124,7 +88,48 @@ impl<C: Default> Default for Binding<C> {
|
|||
}
|
||||
}
|
||||
|
||||
impl Bind<TuiEvent, Arc<str>> {
|
||||
pub fn load (body: impl Language) -> Usually<Self> {
|
||||
let mut bind: Bind<TuiEvent, Arc<str>> = Bind::new();
|
||||
body.each((), |_, item: &str|if matches!(item.expr().head(), Ok(Some("see"))) {
|
||||
bind.add_ref(item)
|
||||
} else if let Ok(Some(_word)) = item.expr().head().word() {
|
||||
bind.add_def(item)
|
||||
} else {
|
||||
return err!("Binds::add: unexpected: {item:?}")
|
||||
})?;
|
||||
Ok(bind)
|
||||
}
|
||||
|
||||
fn add_ref (&mut self, _item: &str) -> Usually<()> {
|
||||
// TODO
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn add_def (&mut self, item: &str) -> Usually<()> {
|
||||
let expr = item.expr()?;
|
||||
let head = expr.head()?;
|
||||
if let Some(event) = TuiKey::from_dsl(&head)?.to_crossterm() {
|
||||
let mut commands: Vec<Arc<str>> = vec![];
|
||||
item.expr()?.tail()?.each((), |_, def|Ok(commands.push(def.into())))?;
|
||||
self.add(TuiEvent(event), Binding {
|
||||
commands: commands.into(),
|
||||
condition: None,
|
||||
description: None,
|
||||
source: None
|
||||
});
|
||||
Ok(())
|
||||
} else if Some(":char") == item.expr()?.head()? {
|
||||
// TODO
|
||||
Ok(())
|
||||
} else {
|
||||
err!("Binds::add: invalid key: {:?}", item.expr()?.head()?)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Clone + Ord, C> Bind<E, C> {
|
||||
|
||||
/// Create a new event map
|
||||
pub fn new () -> Self {
|
||||
Default::default()
|
||||
|
|
@ -156,5 +161,5 @@ impl<E: Clone + Ord, C> Bind<E, C> {
|
|||
|
||||
impl_debug!(Condition |self, w| { write!(w, "*") });
|
||||
|
||||
//take!(DeviceCommand|state: Project, iter|state.selected_device().as_ref()
|
||||
//take!(DeviceCmd|state: Project, iter|state.selected_device().as_ref()
|
||||
//.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten()));
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use crate::*;
|
|||
/// ```
|
||||
/// let mut modes = tek::Modes::default();
|
||||
/// assert_eq!(modes.len(), 0);
|
||||
/// let _ = modes.add(&":foo", &"(mode (name Foo) (info Bar))");
|
||||
/// let _ = modes.add(&"mode Foo (name Foo) (info Bar)").unwrap();
|
||||
/// assert_eq!(modes.len(), 1);
|
||||
/// ```
|
||||
#[derive(Default, Debug, Clone)]
|
||||
|
|
@ -22,7 +22,7 @@ impl Modes {
|
|||
let body = expr.tail()?.ok_or("mode: missing body")?;
|
||||
let mode = Mode::default();
|
||||
let mode = body.each(mode, |c, s|Mode::add(c, s))?;
|
||||
self.0.try_write().unwrap().insert(name.into(), Arc::new(mode));
|
||||
self.0.write().insert(name.into(), Arc::new(mode));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -33,14 +33,14 @@ impl Modes {
|
|||
|
||||
/// Run something for each mode.
|
||||
pub fn for_each <T> (&self, mut ator: impl FnMut(&str, &Mode)->T) {
|
||||
for (k, v) in self.0.try_read().unwrap().iter() {
|
||||
for (k, v) in self.0.read().iter() {
|
||||
let _ = ator(k.as_ref(), v.as_ref());
|
||||
}
|
||||
}
|
||||
|
||||
/// Count modes.
|
||||
pub fn len (&self) -> usize {
|
||||
self.0.try_read().unwrap().len()
|
||||
self.0.read().len()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -86,7 +86,7 @@ impl Mode {
|
|||
let submode = Mode::default();
|
||||
let submode = body.each(submode, |c,s|Mode::add(c,s))?;
|
||||
let modes = mode.as_mut().modes.clone();
|
||||
modes.0.try_write().unwrap().insert(name.into(), Arc::new(submode));
|
||||
modes.0.write().insert(name.into(), Arc::new(submode));
|
||||
mode
|
||||
},
|
||||
"keys" => {
|
||||
|
|
@ -123,40 +123,9 @@ pub trait HasModeStack {
|
|||
|
||||
impl HasModeStack for App {
|
||||
fn modes (&self) -> RwLockReadGuard<'_, Vec<Arc<str>>> {
|
||||
self.modes.try_read().unwrap()
|
||||
self.modes.read()
|
||||
}
|
||||
fn modes_mut (&mut self) -> RwLockWriteGuard<'_, Vec<Arc<str>>> {
|
||||
self.modes.try_write().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ModeController for T where T: HasModeStack
|
||||
+ Namespace<Vec<Arc<str>>>
|
||||
+ Namespace<Arc<str>>
|
||||
+ Namespace<usize>
|
||||
{}
|
||||
|
||||
#[tek_proc::commands(ModeCommand)]
|
||||
pub trait ModeController: HasModeStack
|
||||
+ Namespace<Vec<Arc<str>>>
|
||||
+ Namespace<Arc<str>>
|
||||
+ Namespace<usize>
|
||||
{
|
||||
#[command(Reset = "reset")]
|
||||
fn mode_reset (&mut self, modes: Vec<Arc<str>>) -> Perhaps<ModeCommand> {
|
||||
*self.modes_mut() = modes.into();
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
#[command(Push = "push")]
|
||||
fn mode_push (&mut self, mode: Arc<str>) -> Perhaps<ModeCommand> {
|
||||
self.modes_mut().push(mode);
|
||||
Ok(Some(ModeCommand::Pop))
|
||||
}
|
||||
|
||||
#[command(Pop = "pop")]
|
||||
fn mode_pop (&mut self) -> Perhaps<ModeCommand> {
|
||||
let mode = self.modes_mut().pop();
|
||||
Ok(mode.map(|mode|ModeCommand::Push { mode }))
|
||||
self.modes.write()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -155,18 +155,6 @@ impl Draw<'_, Tui> for Lv2 {
|
|||
break
|
||||
}
|
||||
}
|
||||
draw_header(self, to, x, y, width);
|
||||
draw_lv2_header(self, to, x, y, width);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn draw_header (state: &Lv2, to: &mut Tui, x: u16, y: u16, w: u16) {
|
||||
let style = Style::default().gray();
|
||||
let label1 = format!(" {}", state.name);
|
||||
to.blit(&label1, x + 1, y, Some(style.white().bold()));
|
||||
if let Some(ref path) = state.path {
|
||||
let label2 = format!("{}…", &path[..((w as usize - 10).min(path.len()))]);
|
||||
to.blit(&label2, x + 2 + label1.len() as u16, y, Some(style.not_dim()));
|
||||
}
|
||||
//Ok(Rect { x, y, width: w, height: 1 })
|
||||
}
|
||||
188
src/dialog.rs
188
src/dialog.rs
|
|
@ -1,188 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
pub fn get_dialog <'a> (_: &App, src: impl Language) -> Perhaps<Dialog> {
|
||||
|
||||
eval_word(&src, |word|Ok(match word {
|
||||
|
||||
":none" => Dialog::None,
|
||||
|
||||
":options" => Dialog::Options,
|
||||
|
||||
":device" => Dialog::Device(0),
|
||||
":device/prev" => Dialog::Device(0),
|
||||
":device/next" => Dialog::Device(0),
|
||||
|
||||
":help" => Dialog::Help(0),
|
||||
":save" => Dialog::Browse(
|
||||
BrowseTarget::SaveProject,
|
||||
Browse::new(None).unwrap().into()),
|
||||
":load" => Dialog::Browse(
|
||||
BrowseTarget::LoadProject,
|
||||
Browse::new(None).unwrap().into()),
|
||||
|
||||
":import/clip" => Dialog::Browse(
|
||||
BrowseTarget::ImportClip(Default::default()),
|
||||
Browse::new(None).unwrap().into()),
|
||||
":export/clip" => Dialog::Browse(
|
||||
BrowseTarget::ExportClip(Default::default()),
|
||||
Browse::new(None).unwrap().into()),
|
||||
|
||||
":import/sample" => Dialog::Browse(
|
||||
BrowseTarget::ImportSample(Default::default()),
|
||||
Browse::new(None).unwrap().into()),
|
||||
":export/sample" => Dialog::Browse(
|
||||
BrowseTarget::ExportSample(Default::default()),
|
||||
Browse::new(None).unwrap().into()),
|
||||
|
||||
_ => return Err(format!("not a dialog: {}", src.src()?.unwrap_or("<???>")).into())
|
||||
|
||||
}))
|
||||
|
||||
}
|
||||
|
||||
/// Various possible dialog modes.
|
||||
///
|
||||
/// ```
|
||||
/// let dialog: tek::Dialog = Default::default();
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Default, PartialEq)] pub enum Dialog {
|
||||
#[default] None,
|
||||
Help(usize),
|
||||
Menu(usize, Arc<[MenuItem]>),
|
||||
Device(usize),
|
||||
Message(Arc<str>),
|
||||
Browse(BrowseTarget, Arc<Browse>),
|
||||
Options,
|
||||
}
|
||||
|
||||
impl Dialog {
|
||||
|
||||
/// ```
|
||||
/// let _ = tek::Dialog::welcome();
|
||||
/// ```
|
||||
pub fn welcome () -> Self {
|
||||
Self::Menu(1, Arc::new([
|
||||
|
||||
MenuItem::new(" Resume session ", [
|
||||
AppCommand::Project {
|
||||
command: ProjectCommand::LoadLast
|
||||
}
|
||||
]),
|
||||
|
||||
MenuItem::new(" Create session ", [
|
||||
AppCommand::Dialog {
|
||||
dialog: Dialog::None
|
||||
},
|
||||
AppCommand::Mode {
|
||||
command: ModeCommand::Reset {
|
||||
modes: vec!["Arranger".into()]
|
||||
}
|
||||
},
|
||||
AppCommand::Tracks {
|
||||
command: TracksCommand::Add
|
||||
},
|
||||
AppCommand::Scenes {
|
||||
command: ScenesCommand::Add
|
||||
}
|
||||
]),
|
||||
|
||||
MenuItem::new(" Load session ", [
|
||||
AppCommand::Dialog {
|
||||
dialog: Dialog::Browse(
|
||||
BrowseTarget::LoadProject,
|
||||
Browse::new(None).unwrap().into()
|
||||
)
|
||||
//dialog: Dialog::Browse {
|
||||
//title: "Load session".into(),
|
||||
//filter: "\\.tek$",
|
||||
//}
|
||||
}
|
||||
]),
|
||||
|
||||
MenuItem::new(" Exit ", [
|
||||
AppCommand::Exit
|
||||
]),
|
||||
|
||||
]))
|
||||
}
|
||||
|
||||
/// FIXME: generalize
|
||||
/// ```
|
||||
/// let _ = tek::Dialog::welcome().menu_selected();
|
||||
/// ```
|
||||
pub fn menu_selected (&self) -> Option<usize> {
|
||||
if let Self::Menu(selected, _) = self { Some(*selected) } else { None }
|
||||
}
|
||||
|
||||
/// FIXME: generalize
|
||||
/// ```
|
||||
/// let _ = tek::Dialog::welcome().menu_next();
|
||||
/// ```
|
||||
pub fn menu_next (&self) -> Self {
|
||||
match self {
|
||||
Self::Menu(index, items) => Self::Menu(wrap_inc(*index, items.len()), items.clone()),
|
||||
_ => Self::None
|
||||
}
|
||||
}
|
||||
|
||||
/// FIXME: generalize
|
||||
/// ```
|
||||
/// let _ = tek::Dialog::welcome().menu_prev();
|
||||
/// ```
|
||||
pub fn menu_prev (&self) -> Self {
|
||||
match self {
|
||||
Self::Menu(index, items) => Self::Menu(wrap_dec(*index, items.len()), items.clone()),
|
||||
_ => Self::None
|
||||
}
|
||||
}
|
||||
|
||||
/// FIXME: generalize
|
||||
/// ```
|
||||
/// let _ = tek::Dialog::welcome().device_kind();
|
||||
/// ```
|
||||
pub fn device_kind (&self) -> Option<usize> {
|
||||
if let Self::Device(index) = self { Some(*index) } else { None }
|
||||
}
|
||||
|
||||
/// FIXME: generalize
|
||||
/// ```
|
||||
/// let _ = tek::Dialog::welcome().device_kind_next();
|
||||
/// ```
|
||||
pub fn device_kind_next (&self) -> Option<usize> {
|
||||
self.device_kind().map(|index|(index + 1) % device_kinds().len())
|
||||
}
|
||||
|
||||
/// FIXME: generalize
|
||||
/// ```
|
||||
/// let _ = tek::Dialog::welcome().device_kind_prev();
|
||||
/// ```
|
||||
pub fn device_kind_prev (&self) -> Option<usize> {
|
||||
self.device_kind().map(|index|index.overflowing_sub(1).0.min(device_kinds().len().saturating_sub(1)))
|
||||
}
|
||||
|
||||
/// FIXME: implement
|
||||
pub fn message (&self) -> Option<&str> { todo!() }
|
||||
|
||||
/// FIXME: implement
|
||||
pub fn browser (&self) -> Option<&Arc<Browse>> { todo!() }
|
||||
|
||||
/// FIXME: implement
|
||||
pub fn browser_target (&self) -> Option<&BrowseTarget> { todo!() }
|
||||
|
||||
}
|
||||
|
||||
/// Increment a wrapping counter.
|
||||
pub const fn wrap_inc (index: usize, count: usize) -> usize {
|
||||
if count > 0 { (index + 1) % count } else { 0 }
|
||||
}
|
||||
|
||||
/// Decrement a wrapping counter.
|
||||
pub const fn wrap_dec (index: usize, count: usize) -> usize {
|
||||
if count > 0 {
|
||||
let a = index.overflowing_sub(1).0;
|
||||
let b = count.saturating_sub(1);
|
||||
if a < b { a } else { b }
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
81
src/field.rs
81
src/field.rs
|
|
@ -1,81 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
pub fn field_h <'a, T: Screen> (
|
||||
_theme: ItemTheme, _head: impl Draw<T>, _body: impl Draw<T>
|
||||
) -> impl Draw<T> {
|
||||
}
|
||||
|
||||
pub fn field_v <'a, T: Screen> (
|
||||
_theme: ItemTheme, _head: impl Draw<T>, _body: impl Draw<T>
|
||||
) -> impl Draw<T> {
|
||||
}
|
||||
|
||||
pub fn view_sessions <'a> () -> impl Draw<Tui> {
|
||||
let h = 6;
|
||||
let w = Some(30);
|
||||
let f = Rgb(224, 192, 128);
|
||||
draw(move |to: &mut Tui|{
|
||||
for (index, name) in ["session1", "session2", "session3"].iter().enumerate() {
|
||||
let b = if index == 0 { Rgb(50,50,50) } else { Rgb(40,40,40) };
|
||||
let y = (2 * index) as u16;
|
||||
let h = 2;
|
||||
bg(b, fg(f, *name).align_w()).full_w().exact_h(h).push_y(y).draw(to)?;
|
||||
}
|
||||
Ok(Some(to.area().into()))
|
||||
}).min_w(w).exact_h(h)
|
||||
}
|
||||
|
||||
/// ```
|
||||
/// let bg = tengri::ratatui::style::Color::Red;
|
||||
/// let fg = tengri::ratatui::style::Color::Green;
|
||||
/// let _ = tek::view_wrap(bg, fg, "and then blue, too!");
|
||||
/// ```
|
||||
pub fn view_wrap <'a> (bg: Color, fg: Color, content: impl Draw<Tui>)
|
||||
-> impl Draw<Tui>
|
||||
{
|
||||
let left = fg_bg(bg, Reset, y_repeat("▐").exact_w(1));
|
||||
let right = fg_bg(bg, Reset, y_repeat("▌").exact_w(1));
|
||||
east(left, west(right, fg_bg(fg, bg, content)))
|
||||
}
|
||||
|
||||
/// ```
|
||||
/// let _ = tek::button_2("", "", true);
|
||||
/// let _ = tek::button_2("", "", false);
|
||||
/// ```
|
||||
pub fn button_2 <'a> (key: impl Draw<Tui>, label: impl Draw<Tui>, hide: bool)
|
||||
-> impl Draw<Tui>
|
||||
{
|
||||
let c1 = tui_orange();
|
||||
let c2 = tui_g(0);
|
||||
let c3 = tui_g(96);
|
||||
let c4 = tui_g(255);
|
||||
bold(true, fg_bg(c1, c2,
|
||||
bar(Split::East, 1u16, fg(c2, "▐"),
|
||||
bar(Split::East, 1u16, key,
|
||||
bar(Split::East, 1u16, fg(c3, "▐"),
|
||||
when(!hide, fg_bg(c4, c3, label)))))))
|
||||
}
|
||||
|
||||
/// ```
|
||||
/// let _ = tek::button_3("", "", "", true);
|
||||
/// let _ = tek::button_3("", "", "", false);
|
||||
/// ```
|
||||
pub fn button_3 <'a> (
|
||||
key: impl Draw<Tui>,
|
||||
label: impl Draw<Tui>,
|
||||
value: impl Draw<Tui>,
|
||||
editing: bool,
|
||||
) -> impl Draw<Tui> {
|
||||
let c1 = tui_orange();
|
||||
let c2 = tui_g(0);
|
||||
let c3 = tui_g(96);
|
||||
let c4 = tui_g(255);
|
||||
let c5 = tui_g(128);
|
||||
bold(true, bar(Split::East, 3u16,
|
||||
fg_bg(c1, c2,
|
||||
bar(Split::East, 1u16, fg(c2, "▐"),
|
||||
bar(Split::East, 1u16, key, fg(if editing { c5 } else { c3 }, "▐")))),
|
||||
east(
|
||||
when(!editing, east(fg_bg(c4, c3, label), fg_bg(c5, c3, "▐"),)),
|
||||
east(fg_bg(tui_g(224), c5, value), fg_bg(c5, Reset, "▌"), ))))
|
||||
}
|
||||
54
src/memo.rs
54
src/memo.rs
|
|
@ -1,54 +0,0 @@
|
|||
#![allow(unused)]
|
||||
use crate::*;
|
||||
|
||||
#[macro_export] macro_rules! rewrite {
|
||||
($buf:ident, $($rest:tt)*) => {
|
||||
|$buf,_,_|{
|
||||
$buf.clear();
|
||||
write!($buf, $($rest)*)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub struct Memo<T, U> {
|
||||
value: T,
|
||||
view: U,
|
||||
}
|
||||
|
||||
impl<T, U> Memo<T, U> {
|
||||
pub fn new (value: T, view: U) -> Self {
|
||||
Self { value, view }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: PartialEq + Debug, U: Debug> Debug for Memo<T, U> {
|
||||
fn fmt (&self, _: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: PartialEq, U> Memo<T, U> {
|
||||
pub fn update <R> (&mut self, newval: T, render: impl Fn(&mut U, &T, &T)->R) -> Option<R> {
|
||||
if newval != self.value {
|
||||
let result = render(&mut self.view, &newval, &self.value);
|
||||
self.value = newval;
|
||||
return Some(result);
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Gettable<T> {
|
||||
/// Returns current value
|
||||
fn get (&self) -> T;
|
||||
}
|
||||
|
||||
pub trait Mutable<T>: Gettable<T> {
|
||||
/// Sets new value, returns old
|
||||
fn set (&mut self, value: T) -> T;
|
||||
}
|
||||
|
||||
pub trait InteriorMutable<T>: Gettable<T> {
|
||||
/// Sets new value, returns old
|
||||
fn set (&self, value: T) -> T;
|
||||
}
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
/// An item of a menu.
|
||||
///
|
||||
/// ```
|
||||
/// let item: tek::MenuItem = Default::default();
|
||||
/// ```
|
||||
#[derive(Clone)] pub struct MenuItem(
|
||||
pub Arc<str>,
|
||||
pub Arc<[AppCommand]>,
|
||||
);
|
||||
|
||||
impl MenuItem {
|
||||
pub fn new <const N: usize> (name: impl AsRef<str>, actions: [AppCommand; N]) -> Self {
|
||||
Self(name.as_ref().into(), Arc::new(actions))
|
||||
}
|
||||
}
|
||||
|
||||
impl_debug!(MenuItem |self, w| { write!(w, "{}", &self.0) });
|
||||
|
||||
impl_default!(MenuItem: Self::new("", []));
|
||||
|
||||
impl PartialEq for MenuItem {
|
||||
fn eq (&self, other: &Self) -> bool {
|
||||
self.0 == other.0
|
||||
}
|
||||
}
|
||||
|
||||
//impl AsRef<Arc<[MenuItem]>> for MenuItems {
|
||||
//fn as_ref (&self) -> &Arc<[MenuItem]> {
|
||||
//&self.0
|
||||
//}
|
||||
//}
|
||||
115
src/meter.rs
115
src/meter.rs
|
|
@ -1,115 +0,0 @@
|
|||
#![allow(unused)]
|
||||
use crate::*;
|
||||
|
||||
#[derive(Debug, Default)] pub enum MeteringMode {
|
||||
#[default] Rms,
|
||||
Log10,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct Log10Meter(pub f32);
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct RmsMeter(pub f32);
|
||||
|
||||
impl_draw!(|self: RmsMeter, to: Tui|{
|
||||
let XYWH(x, y, w, h) = to.area().into();
|
||||
let signal = f32::max(0.0, f32::min(100.0, self.0.abs()));
|
||||
let v = (signal * h as f32).ceil() as u16;
|
||||
let y2 = y + h;
|
||||
//to.blit(&format!("\r{v} {} {signal}", self.0), x * 30, y, Some(Style::default()));
|
||||
for y in y..(y + v) {
|
||||
for x in x..(x + w) {
|
||||
to.blit(&"▌", x, y2.saturating_sub(y), Some(Style::default().green()));
|
||||
}
|
||||
}
|
||||
Ok(Some(to.area().into()))
|
||||
});
|
||||
|
||||
impl_draw!(|self: Log10Meter, to: Tui| {
|
||||
let XYWH(x, y, w, h) = to.area().into();
|
||||
let signal = 100.0 - f32::max(0.0, f32::min(100.0, self.0.abs()));
|
||||
let v = (signal * h as f32 / 100.0).ceil() as u16;
|
||||
let y2 = y + h;
|
||||
//to.blit(&format!("\r{v} {} {signal}", self.0), x * 20, y, None);
|
||||
for y in y..(y + v) {
|
||||
for x in x..(x + w) {
|
||||
to.blit(&"▌", x, y2 - y, Some(Style::default().green()));
|
||||
}
|
||||
}
|
||||
Ok(Some(to.area().into()))
|
||||
});
|
||||
|
||||
fn draw_meters (meters: &[f32]) -> impl Draw<Tui> + use<'_> {
|
||||
bg(Black, iter_east_fixed(1, ||meters.iter(), |value, _index|{
|
||||
RmsMeter(*value).full_h()
|
||||
}).exact_w(2))
|
||||
}
|
||||
|
||||
pub fn to_log10 (samples: &[f32]) -> f32 {
|
||||
let total: f32 = samples.iter().map(|x|x.abs()).sum();
|
||||
let count = samples.len() as f32;
|
||||
10. * (total / count).log10()
|
||||
}
|
||||
|
||||
pub fn to_rms (samples: &[f32]) -> f32 {
|
||||
let sum = samples.iter()
|
||||
.map(|s|*s)
|
||||
.reduce(|sum, sample|sum + sample.abs())
|
||||
.unwrap_or(0.0);
|
||||
(sum / samples.len() as f32).sqrt()
|
||||
}
|
||||
|
||||
/// ```
|
||||
/// let _ = tek::view_meter("", 0.0);
|
||||
/// let _ = tek::view_meters(&[0.0, 0.0]);
|
||||
/// ```
|
||||
pub fn view_meter <'a> (label: &'a str, value: f32)
|
||||
-> impl Draw<Tui>
|
||||
{
|
||||
let f = field_h(ItemTheme::G[128], label, format!("{:>+9.3}", value));
|
||||
let w = if value >= 0.0 { 13 }
|
||||
else if value >= -1.0 { 12 }
|
||||
else if value >= -2.0 { 11 }
|
||||
else if value >= -3.0 { 10 }
|
||||
else if value >= -4.0 { 9 }
|
||||
else if value >= -6.0 { 8 }
|
||||
else if value >= -9.0 { 7 }
|
||||
else if value >= -12.0 { 6 }
|
||||
else if value >= -15.0 { 5 }
|
||||
else if value >= -20.0 { 4 }
|
||||
else if value >= -25.0 { 3 }
|
||||
else if value >= -30.0 { 2 }
|
||||
else if value >= -40.0 { 1 }
|
||||
else { 0 };
|
||||
let c = if value >= 0.0 { Red }
|
||||
else if value >= -3.0 { Yellow }
|
||||
else { Green };
|
||||
south!(f, bg(c, ()).exact_wh(w, 1))
|
||||
}
|
||||
|
||||
pub fn view_meters (values: &[f32;2]) -> impl Draw<Tui> + use<'_> {
|
||||
let left = format!("L/{:>+9.3}", values[0]);
|
||||
let right = format!("R/{:>+9.3}", values[1]);
|
||||
south(left, right)
|
||||
}
|
||||
|
||||
#[cfg(test)] mod test_view_meter {
|
||||
use super::*;
|
||||
use proptest::prelude::*;
|
||||
proptest! {
|
||||
|
||||
#[test] fn proptest_view_meter (
|
||||
label in "\\PC*", value in f32::MIN..f32::MAX
|
||||
) {
|
||||
let _ = view_meter(&label, value);
|
||||
}
|
||||
|
||||
#[test] fn proptest_view_meters (
|
||||
value1 in f32::MIN..f32::MAX,
|
||||
value2 in f32::MIN..f32::MAX
|
||||
) {
|
||||
let _ = view_meters(&[value1, value2]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -131,50 +131,6 @@ pub type MidiData = Vec<Vec<MidiMessage>>;
|
|||
impl PartialEq for MidiClip { fn eq (&self, other: &Self) -> bool { self.uuid == other.uuid } }
|
||||
impl Eq for MidiClip {}
|
||||
|
||||
impl HasMidiClip for Project {
|
||||
fn clip (&self) -> Option<Arc<RwLock<MidiClip>>> { None }
|
||||
}
|
||||
impl HasMidiClip for App {
|
||||
fn clip (&self) -> Option<Arc<RwLock<MidiClip>>> { self.project.clip() }
|
||||
}
|
||||
|
||||
impl<T: HasMidiClip
|
||||
+ Namespace<Option<bool>>
|
||||
+ Namespace<Option<ItemTheme>>
|
||||
> MidiClipController for T {}
|
||||
|
||||
#[tek_proc::commands(MidiClipCommand)]
|
||||
pub trait MidiClipController: HasMidiClip
|
||||
+ Namespace<Option<bool>>
|
||||
+ Namespace<Option<ItemTheme>>
|
||||
{
|
||||
|
||||
/// Create a clip at the current position if it does not already exist.
|
||||
#[command(Upsert = "upsert")]
|
||||
fn clip_upsert (&mut self) -> Perhaps<MidiClipCommand> {
|
||||
let clip = self.clip();
|
||||
//let old = self.scenes[scene].clips[track].clone();
|
||||
//self.scenes[scene].clips[track] = clip;
|
||||
//old
|
||||
todo!()
|
||||
}
|
||||
|
||||
#[command(SetColor = "color")]
|
||||
fn clip_set_color (&mut self, color: Option<ItemTheme>) -> Perhaps<MidiClipCommand> {
|
||||
Ok(self.clip().map(|clip|MidiClipCommand::SetColor {
|
||||
color: Some(clip.write().set_color(color))
|
||||
}))
|
||||
}
|
||||
|
||||
/// Toggle looping for the active clip
|
||||
#[command(SetLoop = "loop")]
|
||||
fn clip_toggle_loop (&mut self, looping: Option<bool>) -> Perhaps<MidiClipCommand> {
|
||||
Ok(self.clip().map(|clip|MidiClipCommand::SetLoop {
|
||||
looping: Some(clip.write().toggle_loop(looping))
|
||||
}))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl HasClipsSize for App { fn clips_size (&self) -> &Sizer { &self.project.size_inner } }
|
||||
impl HasClipsSize for Project { fn clips_size (&self) -> &Sizer { &self.size_inner } }
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
use crate::*;
|
||||
143
src/midi_edit.rs
143
src/midi_edit.rs
|
|
@ -19,140 +19,17 @@ pub trait HasEditor: AsRef<Option<MidiEditor>> + AsMut<Option<MidiEditor>> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<T> HasEditor for T where T:
|
||||
AsRef<Option<MidiEditor>> +
|
||||
AsMut<Option<MidiEditor>> {}
|
||||
|
||||
impl<T: HasEditor
|
||||
+ Namespace<u32>
|
||||
+ Namespace<f64>
|
||||
+ Namespace<bool>
|
||||
+ Namespace<usize>
|
||||
+ Namespace<Option<u32>>
|
||||
+ Namespace<Option<Arc<RwLock<MidiClip>>>>
|
||||
> MidiEditController for T {}
|
||||
|
||||
#[tek_proc::commands(MidiEditCommand)]
|
||||
pub trait MidiEditController: HasEditor
|
||||
+ Namespace<u32>
|
||||
+ Namespace<f64>
|
||||
+ Namespace<bool>
|
||||
+ Namespace<usize>
|
||||
+ Namespace<Option<u32>>
|
||||
+ Namespace<Option<Arc<RwLock<MidiClip>>>>
|
||||
{
|
||||
#[command(Toggle = "toggle")]
|
||||
fn edit_toggle (&mut self) -> Perhaps<MidiEditCommand> {
|
||||
if self.is_editing() {
|
||||
*self.editor_mut() = None;
|
||||
} else {
|
||||
todo!()
|
||||
}
|
||||
Ok(Some(MidiEditCommand::Toggle))
|
||||
}
|
||||
|
||||
#[command(Show = "show")]
|
||||
fn edit_show (&mut self, clip: Option<Arc<RwLock<MidiClip>>>) -> Perhaps<MidiEditCommand> {
|
||||
Ok(self.editor_mut().as_mut().map(|mut editor|{
|
||||
editor.set_clip(clip.as_ref());
|
||||
editor.redraw();
|
||||
None
|
||||
}).flatten())
|
||||
}
|
||||
|
||||
#[command(DeleteNote = "delete")]
|
||||
fn edit_note_delete (&mut self) -> Perhaps<MidiEditCommand> {
|
||||
Ok(self.editor_mut().as_mut().map(|editor|{
|
||||
editor.redraw();
|
||||
todo!()
|
||||
}).flatten())
|
||||
}
|
||||
|
||||
#[command(AppendNote = "append")]
|
||||
fn edit_note_append (&mut self, advance: bool) -> Perhaps<MidiEditCommand> {
|
||||
Ok(self.editor_mut().as_mut().map(|mut editor|{
|
||||
editor.put_note(advance);
|
||||
editor.redraw();
|
||||
None
|
||||
}).flatten())
|
||||
}
|
||||
|
||||
#[command(SetNotePos = "note-pos")]
|
||||
fn edit_note_set_pos (&mut self, pos: usize) -> Perhaps<MidiEditCommand> {
|
||||
Ok(self.editor_mut().as_mut().map(|editor|{
|
||||
editor.set_note_pos((pos).min(127));
|
||||
editor.redraw();
|
||||
None
|
||||
}).flatten())
|
||||
}
|
||||
|
||||
#[command(SetNoteLen = "note-len")]
|
||||
fn edit_note_set_len (&mut self, len: usize) -> Perhaps<MidiEditCommand> {
|
||||
Ok(self.editor_mut().as_mut().map(|editor|{
|
||||
editor.set_note_len(len);
|
||||
editor.redraw();
|
||||
None
|
||||
}).flatten())
|
||||
}
|
||||
|
||||
#[command(SetNoteScroll = "note-scroll")]
|
||||
fn edit_note_set_scroll (&mut self, scroll: usize) -> Perhaps<MidiEditCommand> {
|
||||
Ok(self.editor_mut().as_mut().map(|editor|{
|
||||
editor.set_note_lo((scroll).min(127));
|
||||
editor.redraw();
|
||||
None
|
||||
}).flatten())
|
||||
}
|
||||
|
||||
#[command(SetTimePos = "time-pos")]
|
||||
fn edit_time_set_pos (&mut self, pos: usize) -> Perhaps<MidiEditCommand> {
|
||||
Ok(self.editor_mut().as_mut().map(|editor|{
|
||||
editor.set_time_pos(pos);
|
||||
editor.redraw();
|
||||
None
|
||||
}).flatten())
|
||||
}
|
||||
|
||||
#[command(SetTimeScroll = "time-scroll")]
|
||||
fn edit_time_set_scroll (&mut self, scroll: usize) -> Perhaps<MidiEditCommand> {
|
||||
Ok(self.editor_mut().as_mut().map(|editor|{
|
||||
editor.set_time_start(scroll);
|
||||
editor.redraw();
|
||||
None
|
||||
}).flatten())
|
||||
}
|
||||
|
||||
#[command(SetTimeZoom = "time-zoom")]
|
||||
fn edit_time_set_zoom (&mut self, zoom: usize) -> Perhaps<MidiEditCommand> {
|
||||
Ok(self.editor_mut().as_mut().map(|editor|{
|
||||
editor.set_time_zoom(zoom);
|
||||
editor.redraw();
|
||||
None
|
||||
}).flatten())
|
||||
}
|
||||
|
||||
#[command(SetTimeLock = "time-lock")]
|
||||
fn edit_time_set_lock (&mut self, lock: bool) -> Perhaps<MidiEditCommand> {
|
||||
Ok(self.editor_mut().as_mut().map(|editor|{
|
||||
editor.set_time_lock(lock);
|
||||
editor.redraw();
|
||||
None
|
||||
}).flatten())
|
||||
}
|
||||
|
||||
// TODO: 1-9 seek markers that by default start every 8th of the clip
|
||||
}
|
||||
impl<T> HasEditor for T where T: AsRef<Option<MidiEditor>> + AsMut<Option<MidiEditor>> {}
|
||||
|
||||
/// Contains state for viewing and editing a clip.
|
||||
///
|
||||
/// ```
|
||||
/// use std::sync::{Arc, RwLock};
|
||||
/// use std::sync::Arc;
|
||||
/// use parking_lot::RwLock;
|
||||
/// let clip = tek::MidiClip::stop_all();
|
||||
/// let mut editor = tek::MidiEditor {
|
||||
/// mode: tek::PianoHorizontal::new(Some(&Arc::new(RwLock::new(clip)))),
|
||||
/// size: Default::default(),
|
||||
/// //keys: Default::default(),
|
||||
/// };
|
||||
/// let mode = tek::PianoHorizontal::new(Some(&Arc::new(RwLock::new(clip))));
|
||||
/// let mut editor = tek::MidiEditor { mode, size: Default::default() };
|
||||
///
|
||||
/// let _ = editor.put_note(true);
|
||||
/// let _ = editor.put_note(false);
|
||||
/// let _ = editor.clip_status();
|
||||
|
|
@ -275,8 +152,8 @@ impl MidiEditor {
|
|||
/// use tek::{*, tengri::*};
|
||||
///
|
||||
/// struct Test(Option<MidiEditor>);
|
||||
/// impl_as_ref_opt!(MidiEditor: |self: Test|self.0.as_ref());
|
||||
/// impl_as_mut_opt!(MidiEditor: |self: Test|self.0.as_mut());
|
||||
/// impl_as_ref!(Option<MidiEditor>: |self: Test|&self.0);
|
||||
/// impl_as_mut!(Option<MidiEditor>: |self: Test|&mut self.0);
|
||||
///
|
||||
/// let mut host = Test(Some(MidiEditor::default()));
|
||||
/// let _ = host.editor();
|
||||
|
|
@ -427,7 +304,7 @@ impl App {
|
|||
&& slot.is_none()
|
||||
&& let Some(track) = self.project.tracks.get_mut(track)
|
||||
{
|
||||
let (_index, clip) = self.pool.add_new_clip();
|
||||
let (_index, clip) = self.project.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.try_write().unwrap().color = ItemColor::random_near(color, 0.2).into();
|
||||
|
|
@ -450,7 +327,7 @@ impl App {
|
|||
std::mem::swap(&mut swapped, slot);
|
||||
}
|
||||
if let Some(clip) = swapped {
|
||||
self.pool.delete_clip(&clip.try_read().unwrap());
|
||||
self.project.pool.delete_clip(&clip.try_read().unwrap());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ pub trait HasSequencer: AsRef<Sequencer> + AsMut<Sequencer> {
|
|||
///
|
||||
/// let mut clip = tek::MidiClip::new("clip", true, 1, None, None);
|
||||
/// clip.set_length(96);
|
||||
/// clip.toggle_loop();
|
||||
/// clip.toggle_loop(None);
|
||||
/// clip.record_event(12, ::tengri::midly::MidiMessage::NoteOn { key: 36.into(), vel: 100.into() });
|
||||
/// assert!(clip.contains_note_on(36.into(), 6, 18));
|
||||
/// assert_eq!(&clip.notes, &clip.duplicate().notes);
|
||||
|
|
|
|||
42
src/mix.rs
42
src/mix.rs
|
|
@ -1,42 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub enum MixingMode {
|
||||
#[default] Summing,
|
||||
Average,
|
||||
}
|
||||
|
||||
pub fn mix_summing <const N: usize> (
|
||||
buffer: &mut [Vec<f32>], gain: f32, frames: usize, mut next: impl FnMut()->Option<[f32;N]>,
|
||||
) -> bool {
|
||||
let channels = buffer.len();
|
||||
for index in 0..frames {
|
||||
if let Some(frame) = next() {
|
||||
for (channel, sample) in frame.iter().enumerate() {
|
||||
let channel = channel % channels;
|
||||
buffer[channel][index] += sample * gain;
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
pub fn mix_average <const N: usize> (
|
||||
buffer: &mut [Vec<f32>], gain: f32, frames: usize, mut next: impl FnMut()->Option<[f32;N]>,
|
||||
) -> bool {
|
||||
let channels = buffer.len();
|
||||
for index in 0..frames {
|
||||
if let Some(frame) = next() {
|
||||
for (channel, sample) in frame.iter().enumerate() {
|
||||
let channel = channel % channels;
|
||||
let value = buffer[channel][index];
|
||||
buffer[channel][index] = (value + sample * gain) / 2.0;
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
use crate::*;
|
||||
493
src/pool.rs
493
src/pool.rs
|
|
@ -1,493 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
/// A clip pool.
|
||||
///
|
||||
/// ```
|
||||
/// let pool = tek::Pool::default();
|
||||
/// ```
|
||||
#[derive(Debug)] pub struct Pool {
|
||||
pub visible: bool,
|
||||
/// Selected clip
|
||||
pub clip: AtomicUsize,
|
||||
/// Mode switch
|
||||
pub mode: Option<PoolMode>,
|
||||
/// Embedded file browse
|
||||
#[cfg(feature = "browse")] pub browse: Option<Browse>,
|
||||
/// Collection of MIDI clips.
|
||||
#[cfg(feature = "clip")] pub clips: Arc<RwLock<Vec<Arc<RwLock<MidiClip>>>>>,
|
||||
/// Collection of sound samples.
|
||||
#[cfg(feature = "sampler")] pub samples: Arc<RwLock<Vec<Arc<RwLock<Sample>>>>>,
|
||||
}
|
||||
|
||||
impl HasClips for Pool {
|
||||
fn clip (&self) -> Option<Arc<RwLock<MidiClip>>> {
|
||||
self.clips.read().get(self.clip_index()).map(|c|c.clone())
|
||||
}
|
||||
fn clip_mut (&self) -> &mut Option<Arc<RwLock<MidiClip>>> {
|
||||
todo!()
|
||||
}
|
||||
fn clips <'a> (&'a self) -> RwLockReadGuard<'a, Clips> {
|
||||
self.clips.read()
|
||||
}
|
||||
fn clips_mut <'a> (&'a self) -> RwLockWriteGuard<'a, Clips> {
|
||||
self.clips.write()
|
||||
}
|
||||
}
|
||||
|
||||
/// Displays and edits clip length.
|
||||
#[derive(Clone, Debug, Default)] pub struct ClipLength {
|
||||
/// Pulses per beat (quaver)
|
||||
pub ppq: usize,
|
||||
/// Beats per bar
|
||||
pub bpb: usize,
|
||||
/// Length of clip in pulses
|
||||
pub pulses: usize,
|
||||
/// Selected subdivision
|
||||
pub focus: Option<ClipLengthFocus>,
|
||||
}
|
||||
|
||||
/// Some sort of wrapper again?
|
||||
pub struct PoolView<'a>(pub &'a Pool);
|
||||
|
||||
/// 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, Browse),
|
||||
/// Save clip to disk
|
||||
Export(usize, Browse),
|
||||
}
|
||||
|
||||
/// 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<T: AsRef<Pool> + AsMut<Pool>> HasPool for T {}
|
||||
|
||||
pub trait HasPool: AsRef<Pool> + AsMut<Pool> {
|
||||
fn pool (&self) -> &Pool {
|
||||
self.as_ref()
|
||||
}
|
||||
fn pool_mut (&mut self) -> &mut Pool {
|
||||
self.as_mut()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: HasPool
|
||||
+ Namespace<bool>
|
||||
+ Namespace<usize>
|
||||
+ Namespace<Arc<str>>
|
||||
+ Namespace<PathBuf>
|
||||
+ Namespace<MidiClip>
|
||||
+ Namespace<ItemColor>
|
||||
+ Namespace<BrowseCommand>
|
||||
> PoolController for T {}
|
||||
|
||||
#[tek_proc::commands(PoolCommand = "pool")]
|
||||
pub trait PoolController: HasPool
|
||||
+ Namespace<bool>
|
||||
+ Namespace<usize>
|
||||
+ Namespace<Arc<str>>
|
||||
+ Namespace<PathBuf>
|
||||
+ Namespace<MidiClip>
|
||||
+ Namespace<ItemColor>
|
||||
+ Namespace<BrowseCommand>
|
||||
{
|
||||
|
||||
#[command(Show = "show")]
|
||||
/// Toggle visibility of pool
|
||||
fn show (&mut self, visible: bool) -> Perhaps<PoolCommand> {
|
||||
self.pool_mut().visible = visible;
|
||||
Ok(Some(PoolCommand::Show { visible: !visible }))
|
||||
}
|
||||
|
||||
/// Set selected clip
|
||||
#[command(Select = "select")]
|
||||
fn select (&mut self, index: usize) -> Perhaps<PoolCommand> {
|
||||
self.pool_mut().set_clip_index(index);
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
///// Import from file
|
||||
//#[command(Browse = "browse")]
|
||||
//fn browse (&mut self, command: BrowseCommand) -> Perhaps<PoolCommand> {
|
||||
//Ok(if let Some(browse) = self.pool_mut().browse.as_mut() {
|
||||
//command.act(browse)?.map(|command|PoolCommand::Browse{command})
|
||||
//} else {
|
||||
//None
|
||||
//})
|
||||
//}
|
||||
|
||||
/// Import clip to pool from file
|
||||
#[command(Import = "import")]
|
||||
fn clip_import (&mut self, index: usize, path: PathBuf) -> Perhaps<PoolCommand>
|
||||
where Self: Sized
|
||||
{
|
||||
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(PoolCommand::Add { index, clip }.dispatch(self)?)
|
||||
}
|
||||
|
||||
///// Export to file
|
||||
//#[command(Export = "export")]
|
||||
//fn export (&mut self, command: BrowseCommand) -> Perhaps<PoolCommand> {
|
||||
//Ok(if let Some(browse) = self.pool_mut().browse.as_mut() {
|
||||
//command.act(browse)?.map(|command|PoolCommand::Export{command})
|
||||
//} else {
|
||||
//None
|
||||
//})
|
||||
//}
|
||||
|
||||
/// Delete a clip from the pool
|
||||
#[command(Delete = "delete")]
|
||||
fn delete (&mut self, index: usize) -> Perhaps<PoolCommand> {
|
||||
let clip = self.pool_mut().clips_mut().remove(index).try_read().unwrap().clone();
|
||||
Ok(Some(PoolCommand::Add { index, clip }))
|
||||
}
|
||||
|
||||
/// Switch places of two clips in pool
|
||||
#[command(Swap = "swap")]
|
||||
fn swap (&mut self, index: usize, other: usize) -> Perhaps<PoolCommand> {
|
||||
self.pool_mut().clips_mut().swap(index, other);
|
||||
Ok(Some(PoolCommand::Swap { index, other }))
|
||||
}
|
||||
|
||||
/// Add clip to pool
|
||||
#[command(Add = "add")]
|
||||
fn clip_add (&mut self, index: usize, clip: MidiClip) -> Perhaps<PoolCommand> {
|
||||
let mut index = index;
|
||||
let clip = Arc::new(RwLock::new(clip.clone()));
|
||||
let mut clips = self.pool_mut().clips_mut();
|
||||
if index >= clips.len() {
|
||||
index = clips.len();
|
||||
clips.push(clip)
|
||||
} else {
|
||||
clips.insert(index, clip);
|
||||
}
|
||||
Ok(Some(PoolCommand::Delete { index }))
|
||||
}
|
||||
|
||||
/// Set name of clip
|
||||
#[command(SetName = "set-name")]
|
||||
fn clip_set_name (&mut self, index: usize, name: Arc<str>) -> Perhaps<PoolCommand> {
|
||||
let clip = &mut self.pool_mut().clips_mut()[index];
|
||||
let old_name = clip.try_read().unwrap().name.clone();
|
||||
clip.try_write().unwrap().name = name.clone();
|
||||
Ok(Some(PoolCommand::SetName { index, name: old_name }))
|
||||
}
|
||||
|
||||
/// Set length of clip
|
||||
#[command(SetLength = "set-length")]
|
||||
fn clip_set_length (&mut self, index: usize, length: usize) -> Perhaps<PoolCommand> {
|
||||
let clip = &mut self.pool_mut().clips_mut()[index];
|
||||
let old_len = clip.try_read().unwrap().length;
|
||||
clip.try_write().unwrap().length = length;
|
||||
Ok(Some(PoolCommand::SetLength { index, length: old_len }))
|
||||
}
|
||||
|
||||
/// Set color of clip
|
||||
#[command(SetColor = "set-color")]
|
||||
fn clip_set_color (&mut self, index: usize, color: ItemColor) -> Perhaps<PoolCommand> {
|
||||
let mut color = ItemTheme::from(color);
|
||||
std::mem::swap(&mut color, &mut self.pool().clips()[index].try_write().unwrap().color);
|
||||
Ok(Some(PoolCommand::SetColor { index, color: color.base }))
|
||||
}
|
||||
|
||||
/// Enter crop mode
|
||||
#[command(CropBegin = "crop/begin")]
|
||||
fn crop_begin (&mut self) -> Perhaps<PoolCommand> {
|
||||
let index = self.pool().clip_index();
|
||||
let length = self.pool().clips()[index].try_read().unwrap().length;
|
||||
*self.pool_mut().mode_mut() = Some(PoolMode::Length(index, length, ClipLengthFocus::Bar));
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Exit crop mode, discard
|
||||
#[command(CropCancel = "crop/cancel")]
|
||||
fn crop_cancel (&mut self) -> Perhaps<PoolCommand> {
|
||||
if let Some(PoolMode::Length(..)) = self.pool_mut().mode_mut().clone() {
|
||||
*self.pool_mut().mode_mut() = None;
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
#[command(CropSet = "crop/set")]
|
||||
fn crop_set (&mut self, _length: usize) -> Perhaps<PoolCommand> {
|
||||
if let Some(PoolMode::Length(clip, ref mut length, ref mut _focus))
|
||||
= self.pool_mut().mode_mut().clone()
|
||||
{
|
||||
let old_length;
|
||||
{
|
||||
let clip = self.pool().clips()[clip].clone();//.try_write().unwrap();
|
||||
old_length = Some(clip.try_read().unwrap().length);
|
||||
clip.try_write().unwrap().length = *length;
|
||||
}
|
||||
*self.pool_mut().mode_mut() = None;
|
||||
return Ok(old_length.map(|l|PoolCommand::CropSet { _length: l }))
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
#[command(CropNext = "crop/next")]
|
||||
fn crop_next (&mut self) -> Perhaps<PoolCommand> {
|
||||
if let Some(PoolMode::Length(
|
||||
_clip, ref mut _length, ref mut focus
|
||||
)) = self.pool_mut().mode_mut().clone() {
|
||||
focus.next()
|
||||
};
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
#[command(CropPrev = "crop/prev")]
|
||||
fn crop_prev (&mut self) -> Perhaps<PoolCommand> {
|
||||
if let Some(PoolMode::Length(
|
||||
_clip, ref mut _length, ref mut focus
|
||||
)) = self.pool_mut().mode_mut().clone() {
|
||||
focus.prev()
|
||||
};
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
#[command(CropInc = "crop/inc")]
|
||||
fn crop_inc (&mut self) -> Perhaps<PoolCommand> {
|
||||
if let Some(PoolMode::Length(
|
||||
_clip, ref mut length, ref mut focus
|
||||
)) = self.pool_mut().mode_mut().clone() {
|
||||
match focus {
|
||||
ClipLengthFocus::Bar => { *length += 4 * PPQ },
|
||||
ClipLengthFocus::Beat => { *length += PPQ },
|
||||
ClipLengthFocus::Tick => { *length += 1 },
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
#[command(CropDec = "crop/dec")]
|
||||
fn crop_dec (&mut self) -> Perhaps<PoolCommand> {
|
||||
if let Some(PoolMode::Length(
|
||||
_clip, ref mut length, ref mut focus
|
||||
)) = self.pool_mut().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)
|
||||
}
|
||||
|
||||
/// Enter rename mode
|
||||
#[command(RenameBegin = "rename/begin")]
|
||||
fn rename_begin (&mut self) -> Perhaps<PoolCommand> {
|
||||
let index = self.pool().clip_index();
|
||||
let name = self.pool().clips()[index].try_read().unwrap().name.clone();
|
||||
*self.pool_mut().mode_mut() = Some(PoolMode::Rename(index, name));
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Exit rename mode, discard
|
||||
#[command(RenameCancel = "rename/cancel")]
|
||||
fn rename_cancel (&mut self) -> Perhaps<PoolCommand> {
|
||||
if let Some(PoolMode::Rename(clip, ref mut old_name)) = self.pool_mut().mode_mut().clone() {
|
||||
self.pool().clips()[clip].try_write().unwrap().name = old_name.clone().into();
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Exit rename mode, update name
|
||||
#[command(RenameConfirm = "rename/confirm")]
|
||||
fn rename_confirm (&mut self) -> Perhaps<PoolCommand> {
|
||||
Ok(if let Some(PoolMode::Rename(_clip, ref mut old_name)) = self.pool_mut().mode_mut().clone() {
|
||||
let old_name = old_name.clone();
|
||||
*self.pool_mut().mode_mut() = None;
|
||||
Some(PoolCommand::RenameSet { value: old_name })
|
||||
} else {
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
#[command(RenameSet = "rename/set")]
|
||||
fn rename_set (&mut self, value: Arc<str>) -> Perhaps<PoolCommand> {
|
||||
if let Some(PoolMode::Rename(clip, ref mut _old_name)) = self.pool_mut().mode_mut().clone() {
|
||||
self.pool().clips()[clip].try_write().unwrap().name = value.clone();
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl_from!(Pool: |clip:&Arc<RwLock<MidiClip>>|{
|
||||
let model = Self::default();
|
||||
model.clips.try_write().unwrap().push(clip.clone());
|
||||
model.clip.store(1, Relaxed);
|
||||
model
|
||||
});
|
||||
|
||||
impl_default!(Pool: Self {
|
||||
browse: None,
|
||||
clip: 0.into(),
|
||||
clips: Arc::from(RwLock::from(vec![])),
|
||||
mode: None,
|
||||
samples: Arc::from(RwLock::from(vec![])),
|
||||
visible: true,
|
||||
});
|
||||
|
||||
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_import (&mut self) -> Usually<()> {
|
||||
*self.mode_mut() = Some(PoolMode::Import(
|
||||
self.clip_index(),
|
||||
Browse::new(None)?
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
pub fn begin_export (&mut self) -> Usually<()> {
|
||||
*self.mode_mut() = Some(PoolMode::Export(
|
||||
self.clip_index(),
|
||||
Browse::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].try_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.try_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.try_read().unwrap().iter().position(|x|*x.try_read().unwrap()==*clip);
|
||||
if let Some(index) = index {
|
||||
self.clips.try_write().unwrap().remove(index);
|
||||
return true
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
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, }
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
impl Pool {
|
||||
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() }
|
||||
}
|
||||
|
||||
impl<'a> PoolView<'a> {
|
||||
//fn tui (&self) -> impl Draw<'_, Tui> {
|
||||
//let Self(pool) = self;
|
||||
////let color = self.1.clip().map(|c|c.try_read().unwrap().color).unwrap_or_else(||g(32).into());
|
||||
////let on_bg = |x|x;//below(Repeat(" "), bg(color.darkest.term, x));
|
||||
////let border = |x|x;//Outer(Style::default().fg(color.dark.term).bg(color.darkest.term)).enclose(x);
|
||||
////let height = pool.clips.try_read().unwrap().len() as u16;
|
||||
//iter(
|
||||
//||pool.clips().clone().into_iter(),
|
||||
//move|clip: Arc<RwLock<MidiClip>>, i: usize|{
|
||||
//let MidiClip { ref name, color, length, .. } = *clip.try_read().unwrap();
|
||||
//let item_height = 1;
|
||||
//let _item_offset = i as u16 * item_height;
|
||||
//let selected = i == pool.clip_index();
|
||||
//let b = if selected { color.light.term } else { color.base.term };
|
||||
//let f = color.lightest.term;
|
||||
//let name = if false { format!(" {i:>3}") } else { format!(" {i:>3} {name}") };
|
||||
//let length = if false { String::default() } else { format!("{length} ") };
|
||||
//bg(b, below!(
|
||||
//fg(f, bold(selected, name)).origin_w().full_w(),
|
||||
//fg(f, bold(selected, length)).origin_e().full_w(),
|
||||
//when(selected, bold(true, fg(g(255), "▶"))).origin_w().full_w(),
|
||||
//when(selected, bold(true, fg(g(255), "◀"))).origin_e().full_w(),
|
||||
//)).exact_h(1)
|
||||
//}).origin_n().full_h().exact_w(20)
|
||||
//}
|
||||
}
|
||||
|
||||
impl ClipLength {
|
||||
//fn tui (&self) -> impl Draw<'_, Tui> {
|
||||
//use ClipLengthFocus::*;
|
||||
//let bars = format!("{}", self.bars());
|
||||
//let beats = format!("{}", self.beats());
|
||||
//let ticks = format!("{:>02}", self.ticks());
|
||||
//match self.focus {
|
||||
//None => east!(" ", bars, ".", beats, ".", ticks),
|
||||
//Some(Bar) => east!("[", bars, "]", beats, ".", ticks),
|
||||
//Some(Beat) => east!(" ", bars, "[", beats, "]", ticks),
|
||||
//Some(Tick) => east!(" ", bars, ".", beats, "[", ticks),
|
||||
//}
|
||||
//}
|
||||
}
|
||||
87
src/port.rs
87
src/port.rs
|
|
@ -1,87 +0,0 @@
|
|||
use crate::*;
|
||||
def_sizes_iter!(PortsSizes => Arc<str>, [Connect]);
|
||||
impl_has!(Vec<MidiInput>: |self: Project| self.midi_ins);
|
||||
impl_has!(Vec<MidiOutput>: |self: Project| self.midi_outs);
|
||||
impl_has!(Vec<MidiInput>: |self: App|self.project.midi_ins);
|
||||
impl_has!(Vec<MidiOutput>: |self: App|self.project.midi_outs);
|
||||
|
||||
pub fn view_midi_ins_status (theme: ItemTheme, track: Option<&Track>) -> impl Draw<Tui> {
|
||||
track.map(move|track|view_ports_status(theme, "MIDI ins: ", &track.sequencer.midi_ins))
|
||||
}
|
||||
|
||||
pub fn view_midi_outs_status (theme: ItemTheme, track: Option<&Track>) -> impl Draw<Tui> {
|
||||
track.map(move|track|view_ports_status(theme, "MIDI outs: ", &track.sequencer.midi_outs))
|
||||
}
|
||||
|
||||
pub fn view_audio_ins_status (theme: ItemTheme, track: Option<&Track>) -> impl Draw<Tui> {
|
||||
track.map(move|track|view_ports_status(theme, "Audio ins: ", &track.audio_ins()))
|
||||
}
|
||||
|
||||
pub fn view_audio_outs_status (theme: ItemTheme, track: Option<&Track>) -> impl Draw<Tui> {
|
||||
track.map(move|track|view_ports_status(theme, "Audio outs:", &track.audio_outs()))
|
||||
}
|
||||
|
||||
pub fn view_ports_status <'a, T: JackPort> (theme: ItemTheme, title: &'a str, ports: &'a [T])
|
||||
-> impl Draw<Tui> + use<'a, T>
|
||||
{
|
||||
let ins = ports.len() as u16;
|
||||
let frame = Outer(true, Style::default().fg(g(96)));
|
||||
border(true, frame, field_v(theme, title, iter_south({
|
||||
move||ports.iter().enumerate().map(|(index, port)|{
|
||||
east!(" ", index, " ", port.port_name()).align_w().full_h()
|
||||
})
|
||||
})).exact_wh(20, 1 + ins)).exact_wh(20, 1 + ins)
|
||||
}
|
||||
|
||||
pub fn view_io_ports <'a, T: PortsSizes<'a>> (
|
||||
fg: Color, bg: Color, items: impl Fn()->T + Send + Sync + 'a
|
||||
) -> impl Draw<Tui> + 'a {
|
||||
type Item<'a> = (usize, &'a Arc<str>, &'a [Connect], usize, usize);
|
||||
iter(items,
|
||||
move|(_index, name, connections, y, y2): Item<'a>, _| south(
|
||||
bold(true, fg_bg(fg, bg, east(" ", name).align_w())).full_h(),
|
||||
iter(||connections.iter(), move|connect: &'a Connect, index|{
|
||||
bold(false, fg_bg(fg, bg, &connect.info)).exact_h(1).align_w().push_y(index as u16)
|
||||
})
|
||||
).exact_h((y2 - y) as u16).push_y(y as u16))
|
||||
}
|
||||
|
||||
pub struct Junction<T: JackPort>(T);
|
||||
|
||||
impl<T: JackPort> Draw<Tui> for Junction<T> {
|
||||
fn draw (&self, to: &mut Tui) -> Drawn<u16> {
|
||||
T::KIND.draw(to)
|
||||
}
|
||||
}
|
||||
|
||||
#[tek_proc::command(AudioInput)]
|
||||
#[tek_proc::keyword(AudioInput)]
|
||||
#[derive(Debug)]
|
||||
pub enum AudioInputCommand {
|
||||
Close,
|
||||
Connect(Arc<str>),
|
||||
}
|
||||
|
||||
#[tek_proc::command(AudioOutput)]
|
||||
#[tek_proc::keyword(AudioOutput)]
|
||||
#[derive(Debug)]
|
||||
pub enum AudioOutputCommand {
|
||||
Close,
|
||||
Connect(Arc<str>),
|
||||
}
|
||||
|
||||
#[tek_proc::command(MidiInput)]
|
||||
#[tek_proc::keyword(MidiInput)]
|
||||
#[derive(Debug)]
|
||||
pub enum MidiInputCommand {
|
||||
Close,
|
||||
Connect(Arc<str>),
|
||||
}
|
||||
|
||||
#[tek_proc::command(MidiOutput)]
|
||||
#[tek_proc::keyword(MidiOutput)]
|
||||
#[derive(Debug)]
|
||||
pub enum MidiOutputCommand {
|
||||
Close,
|
||||
Connect(Arc<str>),
|
||||
}
|
||||
|
|
@ -1,38 +1,9 @@
|
|||
use crate::*;
|
||||
|
||||
impl AsRef<Option<Project>> for App {
|
||||
fn as_ref (&self) -> &Option<Project> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl AsMut<Option<Project>> for App {
|
||||
fn as_mut (&mut self) -> &mut Option<Project> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl_as_ref!(Option<Project>: |self: App|todo!());
|
||||
impl_as_mut!(Option<Project>: |self: App|todo!());
|
||||
pub trait HasProject: AsRef<Option<Project>> + AsMut<Option<Project>> {}
|
||||
|
||||
impl<T: AsRef<Option<Project>> + AsMut<Option<Project>>> HasProject for T {}
|
||||
|
||||
impl<T: HasProject> ProjectController for T {}
|
||||
|
||||
#[tek_proc::commands(ProjectCommand)]
|
||||
pub trait ProjectController: HasProject {
|
||||
|
||||
#[command(LoadLast = "load-last")]
|
||||
fn load_last (&mut self) -> Perhaps<ProjectCommand> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
#[command(Load = "load")]
|
||||
fn load (&mut self) -> Perhaps<ProjectCommand> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Arranger.
|
||||
///
|
||||
/// ```
|
||||
|
|
@ -55,6 +26,8 @@ pub struct Project {
|
|||
pub size_inner: Sizer,
|
||||
/// Selected UI element
|
||||
pub selection: Selection,
|
||||
/// Contains all recently created clips.
|
||||
pub pool: Pool,
|
||||
/// Source of time
|
||||
#[cfg(feature = "clock")] pub clock: Clock,
|
||||
/// Allows one MIDI clip to be edited
|
||||
|
|
@ -137,7 +110,6 @@ impl Project {
|
|||
}
|
||||
}
|
||||
|
||||
impl HasJack<'static> for Project { fn jack (&self) -> &Jack<'static> { &self.jack } }
|
||||
impl_has!(Jack<'static>: |self: Project| self.jack);
|
||||
impl_has!(Sizer: |self: Project| self.size);
|
||||
impl_has!(Clock: |self: Project| self.clock);
|
||||
|
|
@ -147,6 +119,15 @@ impl_as_ref_opt!(Scene: |self: Project| self.selected_scene());
|
|||
impl_as_mut_opt!(Scene: |self: Project| self.selected_scene_mut());
|
||||
impl_as_ref!(Option<MidiEditor>: |self: Project| &self.editor);
|
||||
impl_as_mut!(Option<MidiEditor>: |self: Project| &mut self.editor);
|
||||
impl HasJack<'static> for Project {
|
||||
fn jack (&self) -> &Jack<'static> { &self.jack }
|
||||
}
|
||||
impl HasClips for Project {
|
||||
fn clip (&self) -> Option<Arc<RwLock<MidiClip>>> { todo!() }
|
||||
fn clip_mut (&self) -> &mut Option<Arc<RwLock<MidiClip>>> { todo!() }
|
||||
fn clips (&self) -> RwLockReadGuard<'_, Clips> { todo!() }
|
||||
fn clips_mut (&self) -> RwLockWriteGuard<'_, Clips> { todo!() }
|
||||
}
|
||||
|
||||
pub fn swap_value <T: Clone + PartialEq, U> (
|
||||
target: &mut T, value: &T, returned: impl Fn(T)->U
|
||||
|
|
@ -57,82 +57,7 @@ pub trait HasClock: AsRef<Clock> + AsMut<Clock> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<T: HasClock
|
||||
+ Namespace<u32>
|
||||
+ Namespace<f64>
|
||||
+ Namespace<Option<u32>>
|
||||
> ClockController for T {}
|
||||
|
||||
#[tek_proc::commands(ClockCommand = "clock")]
|
||||
pub trait ClockController: HasClock
|
||||
+ Namespace<u32>
|
||||
+ Namespace<f64>
|
||||
+ Namespace<Option<u32>>
|
||||
{
|
||||
#[command(SeekUsec = "usec")]
|
||||
fn seek_usec (&mut self, usec: f64) -> Perhaps<ClockCommand> {
|
||||
self.clock().playhead.update_from_usec(usec);
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
#[command(SeekSample = "sample")]
|
||||
fn seek_sample (&mut self, sample: f64) -> Perhaps<ClockCommand> {
|
||||
self.clock().playhead.update_from_sample(sample);
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
#[command(SeekPulse = "pulse")]
|
||||
fn seek_pulse (&mut self, pulse: f64) -> Perhaps<ClockCommand> {
|
||||
self.clock().playhead.update_from_pulse(pulse);
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
#[command(SetBpm = "bpm")]
|
||||
fn set_bpm (&mut self, bpm: f64) -> Perhaps<ClockCommand> {
|
||||
Ok(Some(ClockCommand::SetBpm {
|
||||
bpm: self.clock().timebase().bpm.set(bpm)
|
||||
}))
|
||||
}
|
||||
|
||||
#[command(SetQuant = "quant")]
|
||||
fn set_quant (&mut self, quant: f64) -> Perhaps<ClockCommand> {
|
||||
Ok(Some(ClockCommand::SetQuant {
|
||||
quant: self.clock().quant.set(quant)
|
||||
}))
|
||||
}
|
||||
|
||||
#[command(SetSync = "sync")]
|
||||
fn set_sync (&mut self, sync: f64) -> Perhaps<ClockCommand> {
|
||||
Ok(Some(ClockCommand::SetSync {
|
||||
sync: self.clock().sync.set(sync)
|
||||
}))
|
||||
}
|
||||
|
||||
#[command(Play = "play")]
|
||||
fn play (&mut self, position: Option<u32>) -> Perhaps<ClockCommand> {
|
||||
self.clock().play_from(position)?;
|
||||
Ok(None) /* TODO Some(Pause(previousPosition)) */
|
||||
}
|
||||
|
||||
#[command(Pause = "pause")]
|
||||
fn pause (&mut self, position: Option<u32>) -> Perhaps<ClockCommand> {
|
||||
self.clock().pause_at(position)?;
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
#[command(TogglePlayback = "toggle")]
|
||||
fn toggle (&mut self, position: u32) -> Perhaps<ClockCommand> {
|
||||
Ok(if self.clock().is_rolling() {
|
||||
self.clock().pause_at(Some(position))?;
|
||||
None
|
||||
} else {
|
||||
self.clock().play_from(Some(position))?;
|
||||
None
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
//impl<T: HasClock> Act<T> for ClockCommand {
|
||||
//impl<T: HasClock> Act<T> for ClockCmd {
|
||||
//fn act (&self, state: &mut T) -> Perhaps<Self> {
|
||||
//self.act(state.clock_mut()) // awesome
|
||||
//}
|
||||
|
|
@ -449,22 +374,6 @@ impl_time_unit!(Pulse);
|
|||
impl_time_unit!(Bpm);
|
||||
impl_time_unit!(LaunchSync);
|
||||
|
||||
/// ```
|
||||
/// let x = "";
|
||||
/// let _ = tek::view_status(None, x.as_ref(), x.as_ref(), x.as_ref());
|
||||
/// let _ = tek::view_status(Some("".into()), x.as_ref(), x.as_ref(), x.as_ref());
|
||||
/// ```
|
||||
pub fn view_status <'a> (sel: Option<&str>, sr: &str, buf: &str, lat: &str) -> impl Draw<Tui> {
|
||||
let theme = ItemTheme::G[96];
|
||||
let sr = field_h(theme, "SR", sr);
|
||||
let buf = field_h(theme, "Buf", buf);
|
||||
let lat = field_h(theme, "Lat", lat);
|
||||
bg(Black, east!(above(
|
||||
sel.map(|sel|field_h(theme, "Selected", sel)).align_w().full_wh(),
|
||||
east!(sr, buf, lat).align_e().full_wh(),
|
||||
)))
|
||||
}
|
||||
|
||||
/// ```
|
||||
/// let _ = tek::button_play_pause(true, true);
|
||||
/// let _ = tek::button_play_pause(true, false);
|
||||
|
|
@ -485,3 +394,80 @@ pub fn button_play_pause <'a> (playing: bool, compact: bool) -> impl Draw<Tui> {
|
|||
)
|
||||
)
|
||||
}
|
||||
|
||||
/// Contains memoized renders of clock values.
|
||||
///
|
||||
/// Performance optimization.
|
||||
#[derive(Debug)] pub struct ClockView {
|
||||
pub sr: Memo<Option<(bool, f64)>, String>,
|
||||
pub buf: Memo<Option<f64>, String>,
|
||||
pub lat: Memo<Option<f64>, String>,
|
||||
pub bpm: Memo<Option<f64>, String>,
|
||||
pub beat: Memo<Option<f64>, String>,
|
||||
pub time: Memo<Option<f64>, String>,
|
||||
}
|
||||
|
||||
impl_default!(ClockView: {
|
||||
let mut beat = String::with_capacity(16);
|
||||
let _ = write!(beat, "{}", Self::BEAT_EMPTY);
|
||||
let mut time = String::with_capacity(16);
|
||||
let _ = write!(time, "{}", Self::TIME_EMPTY);
|
||||
let mut bpm = String::with_capacity(16);
|
||||
let _ = write!(bpm, "{}", Self::BPM_EMPTY);
|
||||
Self {
|
||||
beat: Memo::new(None, beat),
|
||||
time: Memo::new(None, time),
|
||||
bpm: Memo::new(None, bpm),
|
||||
sr: Memo::new(None, String::with_capacity(16)),
|
||||
buf: Memo::new(None, String::with_capacity(16)),
|
||||
lat: Memo::new(None, String::with_capacity(16)),
|
||||
}
|
||||
});
|
||||
|
||||
impl ClockView {
|
||||
pub const BEAT_EMPTY: &'static str = "-.-.--";
|
||||
pub const TIME_EMPTY: &'static str = "-.---s";
|
||||
pub const BPM_EMPTY: &'static str = "---.---";
|
||||
pub fn update_clock (cache: &Arc<RwLock<Self>>, clock: &Clock, compact: bool) {
|
||||
let rate = clock.timebase.sr.get();
|
||||
let chunk = clock.chunk.load(Relaxed) as f64;
|
||||
let lat = chunk / rate * 1000.;
|
||||
let delta = |start: &Moment|clock.global.usec.get() - start.usec.get();
|
||||
let mut cache = cache.try_write().unwrap();
|
||||
|
||||
cache.buf.update(
|
||||
Some(chunk), rewrite!(buf, "{chunk}")
|
||||
);
|
||||
|
||||
cache.lat.update(
|
||||
Some(lat), rewrite!(buf, "{lat:.1}ms")
|
||||
);
|
||||
|
||||
cache.sr.update(
|
||||
Some((compact, rate)), |buf: &mut String,_,_|{
|
||||
buf.clear();
|
||||
if compact {
|
||||
write!(buf, "{:.1}kHz", rate / 1000.)
|
||||
} else {
|
||||
write!(buf, "{:.0}Hz", rate)
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if let Some(now) = clock.started.try_read().unwrap().as_ref().map(delta) {
|
||||
let pulse = clock.timebase.usecs_to_pulse(now);
|
||||
let time = now/1000000.;
|
||||
let bpm = clock.timebase.bpm.get();
|
||||
cache.beat.update(Some(pulse), |buf, _, _|{
|
||||
buf.clear();
|
||||
clock.timebase.format_beats_1_to(buf, pulse)
|
||||
});
|
||||
cache.time.update(Some(time), rewrite!(buf, "{:.3}s", time));
|
||||
cache.bpm.update(Some(bpm), rewrite!(buf, "{:.3}", bpm));
|
||||
} else {
|
||||
cache.beat.update(None, rewrite!(buf, "{}", ClockView::BEAT_EMPTY));
|
||||
cache.time.update(None, rewrite!(buf, "{}", ClockView::TIME_EMPTY));
|
||||
cache.bpm.update(None, rewrite!(buf, "{}", ClockView::BPM_EMPTY));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -99,7 +99,7 @@ fn tek_jack_event (state: &mut App, event: JackEvent) {
|
|||
#[tek_proc::command(Device)]
|
||||
#[tek_proc::keyword(Device)]
|
||||
#[derive(Debug)]
|
||||
pub enum DeviceCommand {}
|
||||
pub enum DeviceCmd {}
|
||||
|
||||
impl Device {
|
||||
pub fn name (&self) -> &str {
|
||||
|
|
@ -198,15 +198,3 @@ pub trait HasDevices: AsRef<Vec<Device>> + AsMut<Vec<Device>> {
|
|||
self.as_mut()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn view_device <'a> (state: &'a App) -> impl Draw<Tui> {
|
||||
let selected = state.dialog.device_kind().unwrap();
|
||||
south(
|
||||
bold(true, "Add device"),
|
||||
iter_south(move||device_kinds().iter().enumerate().map(move|(i, _label)|{
|
||||
let b = if i == selected { Rgb(64,128,32) } else { Rgb(0,0,0) };
|
||||
let l = if i == selected { "[ " } else { " " };
|
||||
let r = if i == selected { " ]" } else { " " };
|
||||
bg(b, east(l, west(r, "FIXME device name"))).full_w()
|
||||
})))
|
||||
}
|
||||
95
src/proj_mixer.rs
Normal file
95
src/proj_mixer.rs
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
use crate::*;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub enum MixingMode {
|
||||
#[default] Summing,
|
||||
Average,
|
||||
}
|
||||
|
||||
pub fn mix_summing <const N: usize> (
|
||||
buffer: &mut [Vec<f32>], gain: f32, frames: usize, mut next: impl FnMut()->Option<[f32;N]>,
|
||||
) -> bool {
|
||||
let channels = buffer.len();
|
||||
for index in 0..frames {
|
||||
if let Some(frame) = next() {
|
||||
for (channel, sample) in frame.iter().enumerate() {
|
||||
let channel = channel % channels;
|
||||
buffer[channel][index] += sample * gain;
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
pub fn mix_average <const N: usize> (
|
||||
buffer: &mut [Vec<f32>], gain: f32, frames: usize, mut next: impl FnMut()->Option<[f32;N]>,
|
||||
) -> bool {
|
||||
let channels = buffer.len();
|
||||
for index in 0..frames {
|
||||
if let Some(frame) = next() {
|
||||
for (channel, sample) in frame.iter().enumerate() {
|
||||
let channel = channel % channels;
|
||||
let value = buffer[channel][index];
|
||||
buffer[channel][index] = (value + sample * gain) / 2.0;
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)] pub enum MeteringMode {
|
||||
#[default] Rms,
|
||||
Log10,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct Log10Meter(pub f32);
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct RmsMeter(pub f32);
|
||||
|
||||
impl_draw!(|self: RmsMeter, to: Tui|{
|
||||
let XYWH(x, y, w, h) = to.area().into();
|
||||
let signal = f32::max(0.0, f32::min(100.0, self.0.abs()));
|
||||
let v = (signal * h as f32).ceil() as u16;
|
||||
let y2 = y + h;
|
||||
//to.blit(&format!("\r{v} {} {signal}", self.0), x * 30, y, Some(Style::default()));
|
||||
for y in y..(y + v) {
|
||||
for x in x..(x + w) {
|
||||
to.blit(&"▌", x, y2.saturating_sub(y), Some(Style::default().green()));
|
||||
}
|
||||
}
|
||||
Ok(Some(to.area().into()))
|
||||
});
|
||||
|
||||
impl_draw!(|self: Log10Meter, to: Tui| {
|
||||
let XYWH(x, y, w, h) = to.area().into();
|
||||
let signal = 100.0 - f32::max(0.0, f32::min(100.0, self.0.abs()));
|
||||
let v = (signal * h as f32 / 100.0).ceil() as u16;
|
||||
let y2 = y + h;
|
||||
//to.blit(&format!("\r{v} {} {signal}", self.0), x * 20, y, None);
|
||||
for y in y..(y + v) {
|
||||
for x in x..(x + w) {
|
||||
to.blit(&"▌", x, y2 - y, Some(Style::default().green()));
|
||||
}
|
||||
}
|
||||
Ok(Some(to.area().into()))
|
||||
});
|
||||
|
||||
pub fn to_log10 (samples: &[f32]) -> f32 {
|
||||
let total: f32 = samples.iter().map(|x|x.abs()).sum();
|
||||
let count = samples.len() as f32;
|
||||
10. * (total / count).log10()
|
||||
}
|
||||
|
||||
pub fn to_rms (samples: &[f32]) -> f32 {
|
||||
let sum = samples.iter()
|
||||
.map(|s|*s)
|
||||
.reduce(|sum, sample|sum + sample.abs())
|
||||
.unwrap_or(0.0);
|
||||
(sum / samples.len() as f32).sqrt()
|
||||
}
|
||||
236
src/proj_pool.rs
Normal file
236
src/proj_pool.rs
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
use crate::*;
|
||||
|
||||
/// A clip pool.
|
||||
///
|
||||
/// ```
|
||||
/// let pool = tek::Pool::default();
|
||||
/// ```
|
||||
#[derive(Debug)] pub struct Pool {
|
||||
pub visible: bool,
|
||||
/// Selected clip
|
||||
pub clip: AtomicUsize,
|
||||
/// Mode switch
|
||||
pub mode: Option<PoolMode>,
|
||||
/// Embedded file browse
|
||||
#[cfg(feature = "browse")] pub browse: Option<Browse>,
|
||||
/// Collection of MIDI clips.
|
||||
#[cfg(feature = "clip")] pub clips: Arc<RwLock<Vec<Arc<RwLock<MidiClip>>>>>,
|
||||
/// Collection of sound samples.
|
||||
#[cfg(feature = "sampler")] pub samples: Arc<RwLock<Vec<Arc<RwLock<Sample>>>>>,
|
||||
}
|
||||
|
||||
impl HasClips for Pool {
|
||||
fn clip (&self) -> Option<Arc<RwLock<MidiClip>>> {
|
||||
self.clips.read().get(self.clip_index()).map(|c|c.clone())
|
||||
}
|
||||
fn clip_mut (&self) -> &mut Option<Arc<RwLock<MidiClip>>> {
|
||||
todo!()
|
||||
}
|
||||
fn clips <'a> (&'a self) -> RwLockReadGuard<'a, Clips> {
|
||||
self.clips.read()
|
||||
}
|
||||
fn clips_mut <'a> (&'a self) -> RwLockWriteGuard<'a, Clips> {
|
||||
self.clips.write()
|
||||
}
|
||||
}
|
||||
|
||||
/// Displays and edits clip length.
|
||||
#[derive(Clone, Debug, Default)] pub struct ClipLength {
|
||||
/// Pulses per beat (quaver)
|
||||
pub ppq: usize,
|
||||
/// Beats per bar
|
||||
pub bpb: usize,
|
||||
/// Length of clip in pulses
|
||||
pub pulses: usize,
|
||||
/// Selected subdivision
|
||||
pub focus: Option<ClipLengthFocus>,
|
||||
}
|
||||
|
||||
/// Some sort of wrapper again?
|
||||
pub struct PoolView<'a>(pub &'a Pool);
|
||||
|
||||
/// 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, Browse),
|
||||
/// Save clip to disk
|
||||
Export(usize, Browse),
|
||||
}
|
||||
|
||||
/// 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<T: AsRef<Pool> + AsMut<Pool>> HasPool for T {}
|
||||
|
||||
pub trait HasPool: AsRef<Pool> + AsMut<Pool> {
|
||||
fn pool (&self) -> &Pool {
|
||||
self.as_ref()
|
||||
}
|
||||
fn pool_mut (&mut self) -> &mut Pool {
|
||||
self.as_mut()
|
||||
}
|
||||
}
|
||||
|
||||
impl_from!(Pool: |clip:&Arc<RwLock<MidiClip>>|{
|
||||
let model = Self::default();
|
||||
model.clips.try_write().unwrap().push(clip.clone());
|
||||
model.clip.store(1, Relaxed);
|
||||
model
|
||||
});
|
||||
|
||||
impl_default!(Pool: Self {
|
||||
browse: None,
|
||||
clip: 0.into(),
|
||||
clips: Arc::from(RwLock::from(vec![])),
|
||||
mode: None,
|
||||
samples: Arc::from(RwLock::from(vec![])),
|
||||
visible: true,
|
||||
});
|
||||
|
||||
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_import (&mut self) -> Usually<()> {
|
||||
*self.mode_mut() = Some(PoolMode::Import(
|
||||
self.clip_index(),
|
||||
Browse::new(None)?
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
pub fn begin_export (&mut self) -> Usually<()> {
|
||||
*self.mode_mut() = Some(PoolMode::Export(
|
||||
self.clip_index(),
|
||||
Browse::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].try_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.try_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.try_read().unwrap().iter().position(|x|*x.try_read().unwrap()==*clip);
|
||||
if let Some(index) = index {
|
||||
self.clips.try_write().unwrap().remove(index);
|
||||
return true
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
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, }
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
impl Pool {
|
||||
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() }
|
||||
}
|
||||
|
||||
impl<'a> PoolView<'a> {
|
||||
//fn tui (&self) -> impl Draw<'_, Tui> {
|
||||
//let Self(pool) = self;
|
||||
////let color = self.1.clip().map(|c|c.try_read().unwrap().color).unwrap_or_else(||g(32).into());
|
||||
////let on_bg = |x|x;//below(Repeat(" "), bg(color.darkest.term, x));
|
||||
////let border = |x|x;//Outer(Style::default().fg(color.dark.term).bg(color.darkest.term)).enclose(x);
|
||||
////let height = pool.clips.try_read().unwrap().len() as u16;
|
||||
//iter(
|
||||
//||pool.clips().clone().into_iter(),
|
||||
//move|clip: Arc<RwLock<MidiClip>>, i: usize|{
|
||||
//let MidiClip { ref name, color, length, .. } = *clip.try_read().unwrap();
|
||||
//let item_height = 1;
|
||||
//let _item_offset = i as u16 * item_height;
|
||||
//let selected = i == pool.clip_index();
|
||||
//let b = if selected { color.light.term } else { color.base.term };
|
||||
//let f = color.lightest.term;
|
||||
//let name = if false { format!(" {i:>3}") } else { format!(" {i:>3} {name}") };
|
||||
//let length = if false { String::default() } else { format!("{length} ") };
|
||||
//bg(b, below!(
|
||||
//fg(f, bold(selected, name)).origin_w().full_w(),
|
||||
//fg(f, bold(selected, length)).origin_e().full_w(),
|
||||
//when(selected, bold(true, fg(g(255), "▶"))).origin_w().full_w(),
|
||||
//when(selected, bold(true, fg(g(255), "◀"))).origin_e().full_w(),
|
||||
//)).exact_h(1)
|
||||
//}).origin_n().full_h().exact_w(20)
|
||||
//}
|
||||
}
|
||||
|
||||
impl ClipLength {
|
||||
//fn tui (&self) -> impl Draw<'_, Tui> {
|
||||
//use ClipLengthFocus::*;
|
||||
//let bars = format!("{}", self.bars());
|
||||
//let beats = format!("{}", self.beats());
|
||||
//let ticks = format!("{:>02}", self.ticks());
|
||||
//match self.focus {
|
||||
//None => east!(" ", bars, ".", beats, ".", ticks),
|
||||
//Some(Bar) => east!("[", bars, "]", beats, ".", ticks),
|
||||
//Some(Beat) => east!(" ", bars, "[", beats, "]", ticks),
|
||||
//Some(Tick) => east!(" ", bars, ".", beats, "[", ticks),
|
||||
//}
|
||||
//}
|
||||
}
|
||||
46
src/proj_port.rs
Normal file
46
src/proj_port.rs
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
use crate::*;
|
||||
def_sizes_iter!(PortsSizes => Arc<str>, [Connect]);
|
||||
impl_has!(Vec<MidiInput>: |self: Project| self.midi_ins);
|
||||
impl_has!(Vec<MidiOutput>: |self: Project| self.midi_outs);
|
||||
impl_has!(Vec<MidiInput>: |self: App|self.project.midi_ins);
|
||||
impl_has!(Vec<MidiOutput>: |self: App|self.project.midi_outs);
|
||||
|
||||
pub struct Junction<T: JackPort>(T);
|
||||
|
||||
impl<T: JackPort> Draw<Tui> for Junction<T> {
|
||||
fn draw (&self, to: &mut Tui) -> Drawn<u16> {
|
||||
T::KIND.draw(to)
|
||||
}
|
||||
}
|
||||
|
||||
#[tek_proc::command(AudioInput)]
|
||||
#[tek_proc::keyword(AudioInput)]
|
||||
#[derive(Debug)]
|
||||
pub enum AudioInputCmd {
|
||||
Close,
|
||||
Connect(Arc<str>),
|
||||
}
|
||||
|
||||
#[tek_proc::command(AudioOutput)]
|
||||
#[tek_proc::keyword(AudioOutput)]
|
||||
#[derive(Debug)]
|
||||
pub enum AudioOutputCmd {
|
||||
Close,
|
||||
Connect(Arc<str>),
|
||||
}
|
||||
|
||||
#[tek_proc::command(MidiInput)]
|
||||
#[tek_proc::keyword(MidiInput)]
|
||||
#[derive(Debug)]
|
||||
pub enum MidiInputCmd {
|
||||
Close,
|
||||
Connect(Arc<str>),
|
||||
}
|
||||
|
||||
#[tek_proc::command(MidiOutput)]
|
||||
#[tek_proc::keyword(MidiOutput)]
|
||||
#[derive(Debug)]
|
||||
pub enum MidiOutputCmd {
|
||||
Close,
|
||||
Connect(Arc<str>),
|
||||
}
|
||||
|
|
@ -67,55 +67,6 @@ pub trait HasScene: AsRefOpt<Scene> + AsMutOpt<Scene> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<T: HasScene
|
||||
+ Namespace<usize>
|
||||
+ Namespace<Arc<str>>
|
||||
+ Namespace<ItemTheme>
|
||||
> SceneController for T {}
|
||||
|
||||
#[tek_proc::commands(SceneCommand)]
|
||||
pub trait SceneController: HasScene
|
||||
+ Namespace<usize>
|
||||
+ Namespace<Arc<str>>
|
||||
+ Namespace<ItemTheme>
|
||||
{
|
||||
#[command(SetSize = "size")]
|
||||
fn scene_set_size (&mut self, _size: usize) -> Perhaps<SceneCommand>
|
||||
where Self: Namespace<usize>
|
||||
{
|
||||
todo!()
|
||||
}
|
||||
|
||||
#[command(SetZoom = "zoom")]
|
||||
fn scene_set_zoom (&mut self, _size: usize) -> Perhaps<SceneCommand>
|
||||
where Self: Namespace<usize>
|
||||
{
|
||||
todo!()
|
||||
}
|
||||
|
||||
#[command(SetName = "name")]
|
||||
fn scene_set_name (&mut self, name: Arc<str>) -> Perhaps<SceneCommand>
|
||||
where Self: Namespace<Arc<str>>
|
||||
{
|
||||
Ok(self.scene_mut().map(|scene|swap_value(
|
||||
&mut scene.name,
|
||||
&name,
|
||||
|name|SceneCommand::SetName { name }
|
||||
)).transpose()?.flatten())
|
||||
}
|
||||
|
||||
#[command(SetColor = "color")]
|
||||
fn scene_set_color (&mut self, color: ItemTheme) -> Perhaps<SceneCommand>
|
||||
where Self: Namespace<ItemTheme>
|
||||
{
|
||||
Ok(self.scene_mut().map(|scene|swap_value(
|
||||
&mut scene.color,
|
||||
&color,
|
||||
|color|SceneCommand::SetColor { color }
|
||||
)).transpose()?.flatten())
|
||||
}
|
||||
}
|
||||
|
||||
pub type SceneWith<'a, T> = (usize, &'a Scene, usize, usize, T);
|
||||
|
||||
def_sizes_iter!(ScenesSizes => Scene);
|
||||
|
|
@ -170,59 +121,6 @@ pub trait HasScenes: AsRef<Vec<Scene>> + AsMut<Vec<Scene>> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<T: HasScenes
|
||||
+ HasTracks
|
||||
+ HasTrackScroll
|
||||
+ HasSelection
|
||||
+ HasJack<'static>
|
||||
+ Namespace<usize>
|
||||
+ Namespace<Option<ItemTheme>>
|
||||
+ Namespace<Option<Arc<str>>>
|
||||
> ScenesController for T {}
|
||||
|
||||
#[tek_proc::commands(ScenesCommand)]
|
||||
pub trait ScenesController: HasScenes
|
||||
+ HasTracks
|
||||
+ HasTrackScroll
|
||||
+ HasSelection
|
||||
+ HasJack<'static>
|
||||
+ Namespace<usize>
|
||||
+ Namespace<Option<ItemTheme>>
|
||||
+ Namespace<Option<Arc<str>>>
|
||||
{
|
||||
#[command(Add = "add")]
|
||||
fn scenes_add (&mut self) -> Perhaps<ScenesCommand>
|
||||
where Self: HasScenes + HasJack<'static>
|
||||
{
|
||||
let (_index, _) = self.scenes_add_one(None, None)?;
|
||||
Ok(None)
|
||||
}
|
||||
// TODO
|
||||
|
||||
#[command(Select = "select")]
|
||||
fn scene_select (&mut self) -> Perhaps<ScenesCommand> {
|
||||
let scenes = self.scenes().len();
|
||||
let select = self.selection_mut().select_scene(scenes);
|
||||
*self.selection_mut() = select;
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
#[command(Next = "next")]
|
||||
fn scene_select_next (&mut self) -> Perhaps<ScenesCommand> {
|
||||
let scenes = self.scenes().len();
|
||||
let select = self.selection().select_scene_next(scenes);
|
||||
*self.selection_mut() = select;
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
#[command(Prev = "prev")]
|
||||
fn scene_select_prev (&mut self) -> Perhaps<ScenesCommand> {
|
||||
let select = self.selection_mut().select_scene_prev();
|
||||
*self.selection_mut() = select;
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ScenesView: HasEditor + HasSelection + HasSceneScroll + HasClipsSize + Send + Sync {
|
||||
fn h_scenes (&self) -> u16;
|
||||
fn w_side (&self) -> u16;
|
||||
|
|
@ -280,30 +178,6 @@ impl ScenesView for Project {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn view_scene_name <'a> (
|
||||
select: &Selection,
|
||||
editor: Option<&'a MidiEditor>,
|
||||
index: usize,
|
||||
scene: &Scene,
|
||||
editing: bool
|
||||
) -> impl Draw<Tui> {
|
||||
let h = if select.scene() == Some(index) && let Some(_editor) = editor {
|
||||
7
|
||||
} else {
|
||||
Scene::DEFAULT_HEIGHT as u16
|
||||
};
|
||||
let a = east!("·s", index, " ", fg(g(255), bold(true, &scene.name))).align_w().full_w();
|
||||
let b = when(select.scene() == Some(index) && editing, south(
|
||||
editor.as_ref().map(|e|e.clip_status()),
|
||||
editor.as_ref().map(|e|e.edit_status())).align_nw().full_wh());
|
||||
let c = if select.scene() == Some(index) {
|
||||
scene.color.light.term
|
||||
} else {
|
||||
scene.color.base.term
|
||||
};
|
||||
bg(c, south(a, b).align_nw().exact_h(h))
|
||||
}
|
||||
|
||||
pub trait HasSceneScroll: HasScenes {
|
||||
fn scene_scroll (&self) -> usize;
|
||||
}
|
||||
|
|
@ -319,51 +193,3 @@ impl HasSceneScroll for App {
|
|||
self.project.scene_scroll()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn view_scene_name_theme (scene: &Scene, track_index: usize) -> (Arc<str>, ItemTheme) {
|
||||
if let Some(Some(clip)) = &scene.clips.get(track_index) {
|
||||
let clip = clip.try_read().unwrap();
|
||||
(format!(" ⏹ {}", &clip.name).into(), clip.color)
|
||||
} else {
|
||||
(" ⏹ -- ".into(), ItemTheme::G[32])
|
||||
}
|
||||
}
|
||||
|
||||
pub fn view_scene_bg (
|
||||
theme: ItemTheme, select: &Selection, track_index: usize, scene_index: usize
|
||||
) -> (Color, Color) {
|
||||
let mut outline = theme.base.term;
|
||||
(if select.track() == Some(track_index) && select.scene() == Some(scene_index) {
|
||||
outline = theme.lighter.term;
|
||||
theme.light.term
|
||||
} else if select.track() == Some(track_index) || select.scene() == Some(scene_index) {
|
||||
outline = theme.darkest.term;
|
||||
theme.base.term
|
||||
} else {
|
||||
theme.dark.term
|
||||
}, outline)
|
||||
}
|
||||
|
||||
pub fn view_scene_w (
|
||||
track: &Track, select: &Selection, track_index: usize, editor: Option<&MidiEditor>
|
||||
) -> u16 {
|
||||
if select.track() == Some(track_index) && let Some(editor) = editor {
|
||||
(editor.size.w() as usize).max(24).max(track.width) as u16
|
||||
} else {
|
||||
track.width as u16
|
||||
}
|
||||
}
|
||||
|
||||
pub fn view_scene_y (
|
||||
select: &Selection, scene_index: usize, editor: Option<&MidiEditor>
|
||||
) -> u16 {
|
||||
if select.scene() == Some(scene_index) && let Some(editor) = editor {
|
||||
editor.size.h().max(12)
|
||||
} else {
|
||||
Scene::DEFAULT_HEIGHT as u16
|
||||
}
|
||||
}
|
||||
|
||||
pub fn view_scene_sel (select: &Selection, track_index: usize, scene_index: usize, editing: bool) -> bool {
|
||||
editing && select.track() == Some(track_index) && select.scene() == Some(scene_index)
|
||||
}
|
||||
|
|
@ -58,7 +58,7 @@ impl Track {
|
|||
).exact_w((x2 - x1) as u16)
|
||||
}))
|
||||
}
|
||||
pub fn stop (&mut self) -> Perhaps<TrackCommand> {
|
||||
pub fn stop (&mut self) -> Perhaps<TrackCmd> {
|
||||
self.sequencer.enqueue_next(None);
|
||||
Ok(None)
|
||||
}
|
||||
|
|
@ -85,71 +85,6 @@ pub trait HasTrack: AsRefOpt<Track> + AsMutOpt<Track> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<T: HasTrack
|
||||
+ Namespace<usize>
|
||||
+ Namespace<Arc<str>>
|
||||
+ Namespace<ItemTheme>
|
||||
+ Namespace<Option<bool>>
|
||||
> TrackController for T {}
|
||||
|
||||
#[tek_proc::commands(TrackCommand)]
|
||||
pub trait TrackController: HasTrack
|
||||
+ Namespace<usize>
|
||||
+ Namespace<Arc<str>>
|
||||
+ Namespace<ItemTheme>
|
||||
+ Namespace<Option<bool>>
|
||||
{
|
||||
#[command(Stop = "track/stop")]
|
||||
fn track_stop (&mut self) -> Perhaps<TrackCommand> {
|
||||
self.track_mut().map(|track|track.sequencer.enqueue_next(None));
|
||||
Ok(None)
|
||||
}
|
||||
#[command(SetMute = "track/mute")]
|
||||
fn track_set_mute (&mut self, _mute: Option<bool>) -> Perhaps<TrackCommand> {
|
||||
todo!()
|
||||
}
|
||||
#[command(SetSolo = "track/solo")]
|
||||
fn track_set_solo (&mut self, _solo: Option<bool>) -> Perhaps<TrackCommand> {
|
||||
todo!()
|
||||
}
|
||||
#[command(SetSize = "track/size")]
|
||||
fn track_set_size (&mut self, _size: usize) -> Perhaps<TrackCommand> {
|
||||
todo!()
|
||||
}
|
||||
#[command(SetZoom = "track/zoom")]
|
||||
fn track_set_zoom (&mut self, _zoom: usize) -> Perhaps<TrackCommand> {
|
||||
todo!()
|
||||
}
|
||||
#[command(SetName = "track/name")]
|
||||
fn track_set_name (&mut self, name: Arc<str>) -> Perhaps<TrackCommand> {
|
||||
self.track_mut()
|
||||
.map(|track|swap_value(&mut track.name, &name, |name|TrackCommand::SetName { name }))
|
||||
.transpose()
|
||||
.map(Option::flatten)
|
||||
}
|
||||
#[command(SetColor = "track/color")]
|
||||
fn track_set_color (&mut self, color: ItemTheme) -> Perhaps<TrackCommand> {
|
||||
self.track_mut()
|
||||
.map(|track|swap_value(&mut track.color, &color, |color|TrackCommand::SetColor { color }))
|
||||
.transpose()
|
||||
.map(Option::flatten)
|
||||
}
|
||||
#[command(SetRec = "track/rec")]
|
||||
fn track_set_rec (&mut self, rec: Option<bool>) -> Perhaps<TrackCommand> {
|
||||
self.track_mut()
|
||||
.map(|track|toggle_bool(&mut track.sequencer.recording, &rec, |rec|TrackCommand::SetRec { rec }))
|
||||
.transpose()
|
||||
.map(Option::flatten)
|
||||
}
|
||||
#[command(SetMon = "track/mon")]
|
||||
fn track_set_mon (&mut self, mon: Option<bool>) -> Perhaps<TrackCommand> {
|
||||
self.track_mut()
|
||||
.map(|track|toggle_bool(&mut track.sequencer.monitoring, &mon, |mon|TrackCommand::SetMon { mon }))
|
||||
.transpose()
|
||||
.map(Option::flatten)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn track_width (_index: usize, track: &Track) -> u16 {
|
||||
track.width as u16
|
||||
}
|
||||
|
|
@ -260,90 +195,6 @@ pub trait HasTracks: AsRef<Vec<Track>> + AsMut<Vec<Track>> + HasClock + HasTrack
|
|||
}
|
||||
}
|
||||
|
||||
impl<T: HasTracks
|
||||
+ HasScenes
|
||||
+ HasSelection
|
||||
+ HasJack<'static>
|
||||
+ Namespace<usize>
|
||||
+ Namespace<Arc<[Connect]>>
|
||||
+ Namespace<Option<usize>>
|
||||
+ Namespace<Option<Arc<str>>>
|
||||
+ Namespace<Option<ItemTheme>>
|
||||
+ Namespace<Option<Arc<[Connect]>>>
|
||||
+ Namespace<Option<Vec<Option<Arc<RwLock<MidiClip>>>>>>
|
||||
> TracksController for T {}
|
||||
|
||||
#[tek_proc::commands(TracksCommand)]
|
||||
pub trait TracksController: HasTracks
|
||||
+ HasScenes
|
||||
+ HasSelection
|
||||
+ HasJack<'static>
|
||||
+ Namespace<usize>
|
||||
+ Namespace<Arc<[Connect]>>
|
||||
+ Namespace<Option<usize>>
|
||||
+ Namespace<Option<Arc<str>>>
|
||||
+ Namespace<Option<ItemTheme>>
|
||||
+ Namespace<Option<Arc<[Connect]>>>
|
||||
+ Namespace<Option<Vec<Option<Arc<RwLock<MidiClip>>>>>>
|
||||
{
|
||||
#[command(Stop = "stop")]
|
||||
/// Stop all playing clips
|
||||
fn tracks_stop_all (&mut self) -> Perhaps<TracksCommand> {
|
||||
for track in self.tracks_mut().iter_mut() {
|
||||
track.sequencer.enqueue_next(None);
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
#[command(Launch = "launch")]
|
||||
/// Launch multiple clips
|
||||
fn tracks_launch (
|
||||
&mut self, clips: Option<Vec<Option<Arc<RwLock<MidiClip>>>>>
|
||||
) -> Perhaps<TracksCommand> {
|
||||
if let Some(clips) = clips {
|
||||
for (clip, track) in clips.iter().zip(self.tracks_mut()) {
|
||||
track.sequencer.enqueue_next(clip.as_ref());
|
||||
}
|
||||
} else {
|
||||
for track in self.tracks_mut().iter_mut() {
|
||||
track.sequencer.enqueue_next(None);
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
#[command(Add = "add")]
|
||||
fn tracks_add (&mut self) -> Perhaps<TracksCommand>
|
||||
where Self: HasScenes + HasJack<'static>
|
||||
{
|
||||
let (_index, _) = self.tracks_add_one(None, None, [].into(), [].into())?;
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
#[command(Select = "select")]
|
||||
fn track_select (&mut self) -> Perhaps<TracksCommand> {
|
||||
let tracks = self.tracks().len();
|
||||
let select = self.selection_mut().select_track(tracks);
|
||||
*self.selection_mut() = select;
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
#[command(Next = "next")]
|
||||
fn track_select_next (&mut self) -> Perhaps<TracksCommand> {
|
||||
let tracks = self.tracks().len();
|
||||
let select = self.selection().select_track_next(tracks);
|
||||
*self.selection_mut() = select;
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
#[command(Prev = "prev")]
|
||||
fn track_select_prev (&mut self) -> Perhaps<TracksCommand> {
|
||||
let select = self.selection_mut().select_track_prev();
|
||||
*self.selection_mut() = select;
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
impl<
|
||||
T: HasTracks + ScenesView + HasMidiIns + HasMidiOuts + HasTrack
|
||||
> TracksView for T {}
|
||||
|
|
@ -696,31 +547,6 @@ impl HasTrackScroll for App {
|
|||
}
|
||||
}
|
||||
|
||||
fn view_track_row_section <'a> (
|
||||
_theme: ItemTheme,
|
||||
button: impl Draw<Tui>,
|
||||
button_add: impl Draw<Tui>,
|
||||
content: impl Draw<Tui>,
|
||||
) -> impl Draw<Tui> {
|
||||
bar(
|
||||
Split::West,
|
||||
4u16,
|
||||
button_add.align_ne().full_wh(),
|
||||
bar(
|
||||
Split::East,
|
||||
20u16,
|
||||
button.full_wh().align_nw(),
|
||||
content.align_c().full_wh()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
pub fn view_track_header <'a> (theme: ItemTheme, content: impl Draw<Tui>)
|
||||
-> impl Draw<Tui>
|
||||
{
|
||||
bg(theme.darker.term, content.align_e().full_w()).exact_w(12)
|
||||
}
|
||||
|
||||
//pub fn per_track <'a, T: Draw<'_, Tui> + 'a, U: TracksSizes<'a>> (
|
||||
//tracks: impl Fn() -> U + Send + Sync + 'a,
|
||||
//callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a
|
||||
|
|
@ -8,81 +8,6 @@ pub(crate) use symphonia::{
|
|||
},
|
||||
};
|
||||
|
||||
pub trait HasSampler: AsRef<Sampler> + AsMut<Sampler> {
|
||||
fn sampler (&self) -> &Sampler {
|
||||
self.as_ref()
|
||||
}
|
||||
fn sampler_mut (&mut self) -> &mut Sampler {
|
||||
self.as_mut()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: AsRef<Sampler> + AsMut<Sampler>> HasSampler for T {}
|
||||
|
||||
#[tek_proc::commands(SamplerCommand = "sampler")]
|
||||
pub trait SamplerController: HasSampler
|
||||
+ Namespace<usize>
|
||||
{
|
||||
|
||||
#[command(RecordToggle = "rec-toggle")]
|
||||
fn record_toggle (&mut self, slot: usize) -> Perhaps<SamplerCommand>
|
||||
where Self: Sized
|
||||
{
|
||||
let sampler = self.sampler_mut();
|
||||
let recording = sampler.recording.as_ref().map(|x|x.0);
|
||||
let _ = SamplerCommand::RecordFinish.dispatch(self)?;
|
||||
// autoslice: continue recording at next slot
|
||||
if recording != Some(slot) {
|
||||
SamplerCommand::RecordBegin { slot }.dispatch(self)
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
#[command(RecordBegin = "rec-begin")]
|
||||
fn record_begin (&mut self, slot: usize) -> Perhaps<SamplerCommand> {
|
||||
let sampler = self.sampler_mut();
|
||||
sampler.recording = Some((
|
||||
slot,
|
||||
Some(Arc::new(RwLock::new(Sample::new(
|
||||
"Sample", 0, 0, vec![vec![]; sampler.audio_ins.len()]
|
||||
))))
|
||||
));
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
#[command(RecordFinish = "rec-finish")]
|
||||
fn record_finish (&mut self) -> Perhaps<SamplerCommand> {
|
||||
let sampler = self.sampler_mut();
|
||||
let _prev_sample = sampler.recording.as_mut().map(|(index, sample)|{
|
||||
std::mem::swap(sample, &mut sampler.samples.0[*index]);
|
||||
sample
|
||||
}); // TODO: undo
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
#[command(RecordCancel = "rec-cancel")]
|
||||
fn record_cancel (&mut self) -> Perhaps<SamplerCommand> {
|
||||
self.sampler_mut().recording = None;
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
#[command(PlaySample = "sample-play")]
|
||||
fn sample_play (&mut self, slot: usize) -> Perhaps<SamplerCommand> {
|
||||
let sampler = self.sampler_mut();
|
||||
if let Some(ref sample) = sampler.samples.0[slot] {
|
||||
sampler.voices.try_write().unwrap().push(Sample::play(sample, 0, &u7::from(128)));
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
#[command(StopSample = "sample-stop")]
|
||||
fn sample_stop (&mut self, slot: usize) -> Perhaps<SamplerCommand> {
|
||||
todo!();
|
||||
//Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Plays [Voice]s from [Sample]s.
|
||||
///
|
||||
/// ```
|
||||
|
|
@ -133,6 +58,17 @@ pub trait SamplerController: HasSampler
|
|||
#[cfg(feature = "meter")] pub output_meters: Vec<f32>,
|
||||
}
|
||||
|
||||
pub trait HasSampler: AsRef<Sampler> + AsMut<Sampler> {
|
||||
fn sampler (&self) -> &Sampler {
|
||||
self.as_ref()
|
||||
}
|
||||
fn sampler_mut (&mut self) -> &mut Sampler {
|
||||
self.as_mut()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: AsRef<Sampler> + AsMut<Sampler>> HasSampler for T {}
|
||||
|
||||
impl_audio!(Sampler: sampler_jack_process);
|
||||
|
||||
pub(crate) fn sampler_jack_process (state: &mut Sampler, _: &Client, scope: &ProcessScope) -> Control {
|
||||
|
|
@ -314,84 +250,6 @@ impl Sampler {
|
|||
}
|
||||
}
|
||||
|
||||
fn draw_list_item (sample: &Option<Arc<RwLock<Sample>>>) -> String {
|
||||
if let Some(sample) = sample {
|
||||
let sample = sample.try_read().unwrap();
|
||||
format!("{:8}", sample.name)
|
||||
//format!("{:8} {:3} {:6}-{:6}/{:6}",
|
||||
//sample.name,
|
||||
//sample.gain,
|
||||
//sample.start,
|
||||
//sample.end,
|
||||
//sample.channels[0].len()
|
||||
//)
|
||||
} else {
|
||||
String::from("........")
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_viewer (sample: Option<&Arc<RwLock<Sample>>>) -> impl Draw<Tui> + use<'_> {
|
||||
let min_db = -64.0;
|
||||
draw(move|to: &mut Tui|{
|
||||
let xywh = to.area().into();
|
||||
let XYWH(x, y, width, height) = xywh;
|
||||
let area = Rect { x, y, width, height };
|
||||
if let Some(sample) = &sample {
|
||||
let sample = sample.try_read().unwrap();
|
||||
let start = sample.start as f64;
|
||||
let end = sample.end as f64;
|
||||
let length = end - start;
|
||||
let step = length / width as f64;
|
||||
let mut t = start;
|
||||
let mut lines = vec![];
|
||||
while t < end {
|
||||
let chunk = &sample.channels[0][t as usize..((t + step) as usize).min(sample.end)];
|
||||
let total: f32 = chunk.iter().map(|x|x.abs()).sum();
|
||||
let count = chunk.len() as f32;
|
||||
let meter = 10. * (total / count).log10();
|
||||
let x = t as f64;
|
||||
let y = meter as f64;
|
||||
lines.push(Line::new(x, min_db, x, y, Color::Green));
|
||||
t += step / 2.;
|
||||
}
|
||||
to.buffer().map(|buf|{
|
||||
Canvas::default()
|
||||
.x_bounds([sample.start as f64, sample.end as f64])
|
||||
.y_bounds([min_db, 0.])
|
||||
.paint(|ctx| {
|
||||
for line in lines.iter() {
|
||||
ctx.draw(line);
|
||||
}
|
||||
//FIXME: proportions
|
||||
//let text = "press record to finish sampling";
|
||||
//ctx.print(
|
||||
//(width - text.len() as u16) as f64 / 2.0,
|
||||
//height as f64 / 2.0,
|
||||
//text.red()
|
||||
//);
|
||||
})
|
||||
.render(area, buf);
|
||||
});
|
||||
} else {
|
||||
to.buffer().map(|buf|{
|
||||
Canvas::default()
|
||||
.x_bounds([0.0, width as f64])
|
||||
.y_bounds([0.0, height as f64])
|
||||
.paint(|_ctx| {
|
||||
//let text = "press record to begin sampling";
|
||||
//ctx.print(
|
||||
//(width - text.len() as u16) as f64 / 2.0,
|
||||
//height as f64 / 2.0,
|
||||
//text.red()
|
||||
//);
|
||||
})
|
||||
.render(area, buf);
|
||||
});
|
||||
}
|
||||
Ok(Some(xywh))
|
||||
})
|
||||
}
|
||||
|
||||
/// Create [Voice]s from [Sample]s in response to MIDI input.
|
||||
fn sampler_midi_in (
|
||||
samples: &SampleKit<128>, voices: &Arc<RwLock<Vec<Voice>>>, RawMidi { time, bytes }: RawMidi
|
||||
|
|
@ -411,71 +269,6 @@ fn sampler_midi_in (
|
|||
}
|
||||
}
|
||||
|
||||
fn draw_sample (
|
||||
to: &mut Tui, x: u16, y: u16, note: Option<&u7>, sample: &Sample, focus: bool
|
||||
) -> Usually<usize> {
|
||||
let style = if focus { Style::default().green() } else { Style::default() };
|
||||
if focus {
|
||||
to.blit(&"🬴", x+1, y, Some(style.bold()));
|
||||
}
|
||||
let label1 = format!("{:3} {:12}",
|
||||
note.map(|n|n.to_string()).unwrap_or(String::default()),
|
||||
sample.name);
|
||||
let label2 = format!("{:>6} {:>6} +0.0",
|
||||
sample.start,
|
||||
sample.end);
|
||||
to.blit(&label1, x+2, y, Some(style.bold()));
|
||||
to.blit(&label2, x+3+label1.len()as u16, y, Some(style));
|
||||
Ok(label1.len() + label2.len() + 4)
|
||||
}
|
||||
|
||||
pub fn view_sample_info (sample: Option<&Arc<RwLock<Sample>>>) -> impl Draw<Tui> + use<'_> {
|
||||
when(sample.is_some(), draw(move|to: &mut Tui|{
|
||||
let sample = sample.unwrap().try_read().unwrap();
|
||||
let theme = sample.color;
|
||||
east!(
|
||||
field_h(theme, "Name", format!("{:<10}", sample.name.clone())),
|
||||
field_h(theme, "Length", format!("{:<8}", sample.channels[0].len())),
|
||||
field_h(theme, "Start", format!("{:<8}", sample.start)),
|
||||
field_h(theme, "End", format!("{:<8}", sample.end)),
|
||||
field_h(theme, "Trans", "0"),
|
||||
field_h(theme, "Gain", format!("{}", sample.gain)),
|
||||
).draw(to)
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn view_sample_info_v (sample: Option<&Arc<RwLock<Sample>>>) -> impl Draw<Tui> + use<'_> {
|
||||
let a = draw(move|to: &mut Tui|{
|
||||
let sample = sample.unwrap().try_read().unwrap();
|
||||
let theme = sample.color;
|
||||
south!(
|
||||
field_h(theme, "Name ", format!("{:<10}", sample.name.clone())) .align_w().full_w(),
|
||||
field_h(theme, "Length", format!("{:<8}", sample.channels[0].len())).align_w().full_w(),
|
||||
field_h(theme, "Start ", format!("{:<8}", sample.start)) .align_w().full_w(),
|
||||
field_h(theme, "End ", format!("{:<8}", sample.end)) .align_w().full_w(),
|
||||
field_h(theme, "Trans ", "0") .align_w().full_w(),
|
||||
field_h(theme, "Gain ", format!("{}", sample.gain)) .align_w().full_w(),
|
||||
).exact_w(20).draw(to)
|
||||
});
|
||||
|
||||
let b = draw(|to: &mut Tui|fg(Red, south!(
|
||||
bold(true, "× No sample."),
|
||||
"[r] record",
|
||||
"[Shift-F9] import",
|
||||
)).draw(to));
|
||||
|
||||
either(sample.is_some(), a, b)
|
||||
}
|
||||
|
||||
pub fn view_sample_status (sample: Option<&Arc<RwLock<Sample>>>) -> impl Draw<Tui> {
|
||||
bold(true, fg(g(224), sample
|
||||
.map(|sample|{
|
||||
let sample = sample.try_read().unwrap();
|
||||
format!("Sample {}-{}", sample.start, sample.end)
|
||||
})
|
||||
.unwrap_or_else(||"No sample".to_string())))
|
||||
}
|
||||
|
||||
/// Get the first sampler of the active track
|
||||
impl Project {
|
||||
pub fn sampler (&self) -> Option<&Sampler> {
|
||||
780
src/tek.rs
780
src/tek.rs
|
|
@ -1,5 +1,34 @@
|
|||
#![allow(clippy::unit_arg)]
|
||||
|
||||
mod deps; pub use self::deps::*;
|
||||
|
||||
mod config; pub use self::config::*;
|
||||
mod config_bind; pub use self::config_bind::*;
|
||||
mod config_mode; pub use self::config_mode::*;
|
||||
mod config_view; pub use self::config_view::*;
|
||||
|
||||
mod midi_octave; pub use self::midi_octave::*;
|
||||
mod midi_piano; pub use self::midi_piano::*;
|
||||
mod midi_point; pub use self::midi_point::*;
|
||||
mod midi_range; pub use self::midi_range::*;
|
||||
|
||||
mod smpl; pub use self::smpl::*;
|
||||
mod smpl_sample; pub use self::smpl_sample::*;
|
||||
mod smpl_add; pub use self::smpl_add::*;
|
||||
mod smpl_kit; pub use self::smpl_kit::*;
|
||||
mod smpl_voice; pub use self::smpl_voice::*;
|
||||
|
||||
mod midi_clip; pub use self::midi_clip::*;
|
||||
mod midi_edit; pub use self::midi_edit::*;
|
||||
mod midi_mon; pub use self::midi_mon::*;
|
||||
mod midi_play; pub use self::midi_play::*;
|
||||
mod midi_rec; pub use self::midi_rec::*;
|
||||
mod midi_seq; pub use self::midi_seq::*;
|
||||
|
||||
mod time_base; pub use self::time_base::*;
|
||||
mod time_moment; pub use self::time_moment::*;
|
||||
mod time_ticker; pub use self::time_ticker::*;
|
||||
|
||||
/// Define a type alias for iterators of sized items (columns).
|
||||
macro_rules! def_sizes_iter {
|
||||
($Type:ident => $($Item:ty),+) => {
|
||||
|
|
@ -8,57 +37,24 @@ macro_rules! def_sizes_iter {
|
|||
}
|
||||
}
|
||||
|
||||
mod app; pub use self::app::*;
|
||||
mod browse; pub use self::browse::*;
|
||||
mod cli; pub use self::cli::*;
|
||||
mod clock; pub use self::clock::*;
|
||||
mod clock_view; pub use self::clock_view::*;
|
||||
mod config; pub use self::config::*;
|
||||
mod config_bind; pub use self::config_bind::*;
|
||||
mod config_mode; pub use self::config_mode::*;
|
||||
mod config_view; pub use self::config_view::*;
|
||||
mod deps; pub use self::deps::*;
|
||||
mod device; pub use self::device::*;
|
||||
mod dialog; pub use self::dialog::*;
|
||||
mod field; pub use self::field::*;
|
||||
mod memo; pub use self::memo::*;
|
||||
mod menu_item; pub use self::menu_item::*;
|
||||
mod meter; pub use self::meter::*;
|
||||
mod mix; pub use self::mix::*;
|
||||
mod moment; pub use self::moment::*;
|
||||
mod octave; pub use self::octave::*;
|
||||
mod piano; pub use self::piano::*;
|
||||
mod point; pub use self::point::*;
|
||||
mod pool; pub use self::pool::*;
|
||||
mod port; pub use self::port::*;
|
||||
mod project; pub use self::project::*;
|
||||
mod range; pub use self::range::*;
|
||||
mod sampler; pub use self::sampler::*;
|
||||
mod sample; pub use self::sample::*;
|
||||
mod sample_add; pub use self::sample_add::*;
|
||||
mod sample_kit; pub use self::sample_kit::*;
|
||||
mod sample_voice; pub use self::sample_voice::*;
|
||||
mod scene; pub use self::scene::*;
|
||||
mod select; pub use self::select::*;
|
||||
mod midi_clip; pub use self::midi_clip::*;
|
||||
mod midi_clips; pub use self::midi_clips::*;
|
||||
mod midi_edit; pub use self::midi_edit::*;
|
||||
mod midi_mon; pub use self::midi_mon::*;
|
||||
mod midi_play; pub use self::midi_play::*;
|
||||
mod midi_rec; pub use self::midi_rec::*;
|
||||
mod midi_seq; pub use self::midi_seq::*;
|
||||
mod ticker; pub use self::ticker::*;
|
||||
mod timebase; pub use self::timebase::*;
|
||||
mod track; pub use self::track::*;
|
||||
mod proj; pub use self::proj::*;
|
||||
mod proj_clock; pub use self::proj_clock::*;
|
||||
mod proj_device; pub use self::proj_device::*;
|
||||
mod proj_mixer; pub use self::proj_mixer::*;
|
||||
mod proj_port; pub use self::proj_port::*;
|
||||
mod proj_scene; pub use self::proj_scene::*;
|
||||
mod proj_select; pub use self::proj_select::*;
|
||||
mod proj_track; pub use self::proj_track::*;
|
||||
mod proj_pool; pub use self::proj_pool::*;
|
||||
|
||||
#[cfg(feature = "lv2")] mod lv2;
|
||||
#[cfg(feature = "lv2")] pub use self::lv2::*;
|
||||
mod tek_browse; pub use self::tek_browse::*;
|
||||
mod tek_cli; pub use self::tek_cli::*;
|
||||
mod tek_ctrl; pub use self::tek_ctrl::*;
|
||||
mod tek_view; pub use self::tek_view::*;
|
||||
|
||||
#[cfg(feature = "lv2_gui")] mod lv2_gui;
|
||||
#[cfg(feature = "lv2_gui")] pub use self::lv2_gui::*;
|
||||
|
||||
#[cfg(feature = "vst2")] mod vst2;
|
||||
#[cfg(feature = "vst2")] pub use self::vst2::*;
|
||||
#[cfg(feature = "lv2")] mod device_lv2; #[cfg(feature = "lv2")] pub use self::device_lv2::*;
|
||||
#[cfg(feature = "lv2_gui")] mod device_lv2_gui; #[cfg(feature = "lv2_gui")] pub use self::device_lv2_gui::*;
|
||||
#[cfg(feature = "vst2")] mod device_vst2; #[cfg(feature = "vst2")] pub use self::device_vst2::*;
|
||||
|
||||
/// Command-line entrypoint.
|
||||
#[allow(unused)]
|
||||
|
|
@ -102,3 +98,689 @@ fn run_new_plain (config: Config) -> Usually<()> {
|
|||
Ok(app)
|
||||
})?)
|
||||
}
|
||||
|
||||
/// Total application state.
|
||||
///
|
||||
/// ```
|
||||
/// use tek::{HasTracks, HasScenes, TracksView, ScenesView};
|
||||
/// let mut app = tek::App::default();
|
||||
/// let _ = app.scenes_add_one(None, None).unwrap();
|
||||
/// let _ = app.update_clock();
|
||||
/// app.project.editor = Some(Default::default());
|
||||
/// //let _: Vec<_> = app.project.inputs_with_sizes().collect();
|
||||
/// //let _: Vec<_> = app.project.outputs_with_sizes().collect();
|
||||
/// let _: Vec<_> = app.project.tracks_with_sizes().collect();
|
||||
/// //let _: Vec<_> = app.project.scenes_with_sizes(true, 10, 10).collect();
|
||||
/// //let _: Vec<_> = app.scenes_with_colors(true, 10).collect();
|
||||
/// //let _: Vec<_> = app.scenes_with_track_colors(true, 10, 10).collect();
|
||||
/// let _ = app.project.w();
|
||||
/// //let _ = app.project.w_sidebar();
|
||||
/// //let _ = app.project.w_tracks_area();
|
||||
/// let _ = app.project.h();
|
||||
/// //let _ = app.project.h_tracks_area();
|
||||
/// //let _ = app.project.h_inputs();
|
||||
/// //let _ = app.project.h_outputs();
|
||||
/// let _ = app.project.h_scenes();
|
||||
/// ```
|
||||
#[derive(Default, Debug)]
|
||||
#[namespace(Arc<[Connect]> get_arc_array_connect)]
|
||||
#[namespace(Arc<str> get_arc_str)]
|
||||
#[namespace(Color get_color)]
|
||||
#[namespace(ControlAxis get_axis)]
|
||||
#[namespace(Dialog get_dialog)]
|
||||
#[namespace(ItemColor)]
|
||||
#[namespace(ItemTheme)]
|
||||
#[namespace(MidiClip)]
|
||||
#[namespace(Option<Arc<[Connect]>> get_opt_arc_array_connect)]
|
||||
#[namespace(Option<Arc<str>> get_opt_arc_str)]
|
||||
#[namespace(Option<ItemTheme> get_opt_itemtheme)]
|
||||
#[namespace(Option<Arc<RwLock<MidiClip>>> get_opt_arc_rwlock_midiclip)]
|
||||
#[namespace(Option<Vec<Option<Arc<RwLock<MidiClip>>>>> get_opt_vec_opt_arc_rwlock_midiclip)]
|
||||
#[namespace(Option<bool> get_opt_bool)]
|
||||
#[namespace(Option<u16> get_opt_u16)]
|
||||
#[namespace(Option<u32> get_opt_u32)]
|
||||
#[namespace(Option<usize> get_opt_usize)]
|
||||
#[namespace(PathBuf get_path_buf)]
|
||||
#[namespace(Vec<Arc<str>> get_vec_arc_str)]
|
||||
#[namespace(bool get_bool)]
|
||||
#[namespace(f64 get_f64)]
|
||||
#[namespace(isize)]
|
||||
#[namespace(u16 get_u16)]
|
||||
#[namespace(u32)]
|
||||
#[namespace(u8)]
|
||||
#[namespace(usize get_usize)]
|
||||
pub struct App {
|
||||
/// Exit flag
|
||||
pub exit: Exit,
|
||||
/// Base color.
|
||||
pub color: ItemTheme,
|
||||
/// Must not be dropped for the duration of the process
|
||||
pub jack: Jack<'static>,
|
||||
/// Display size
|
||||
pub size: Sizer,
|
||||
/// Performance counter
|
||||
pub perf: PerfModel,
|
||||
/// Available view modes and input bindings
|
||||
pub config: Arc<Config>,
|
||||
/// Currently selected mode
|
||||
pub modes: Arc<RwLock<Vec<Arc<str>>>>,
|
||||
/// Undo history
|
||||
pub history: Vec<(AppCmd, Option<AppCmd>)>,
|
||||
/// Dialog overlay
|
||||
pub dialog: Dialog,
|
||||
/// Contains the currently edited musical arrangement
|
||||
pub project: Project,
|
||||
/// Error, if any
|
||||
pub error: Arc<RwLock<Option<Arc<str>>>>,
|
||||
#[cfg(feature = "prof2")]
|
||||
/// Tracing guard
|
||||
pub guard: Option<tracing_flame::FlushGuard<std::io::BufWriter<std::fs::File>>>,
|
||||
}
|
||||
|
||||
impl App {
|
||||
/// Create a new application instance from a backend, project, config, and mode
|
||||
///
|
||||
/// ```
|
||||
/// let mut proj = tek::Project::default();
|
||||
/// proj.jack = tek::tengri::Jack::new(&"test_tek").expect("failed to connect to jack");
|
||||
/// let config = "(mode hi (name Hello) (info World))";
|
||||
/// let mut conf = tek::Config::load(tek::Config::default(), config).unwrap();
|
||||
/// let tek = tek::App::new(None, proj, conf.into(), "hello");
|
||||
/// ```
|
||||
pub fn new (
|
||||
exit: Option<Exit>,
|
||||
project: Project,
|
||||
config: Arc<Config>,
|
||||
mode: impl AsRef<str>
|
||||
) -> Self {
|
||||
App {
|
||||
exit: exit.unwrap_or_default(),
|
||||
jack: project.jack.clone(),
|
||||
color: ItemTheme::random(),
|
||||
dialog: Dialog::welcome(),
|
||||
modes: Arc::new(RwLock::new(vec![mode.as_ref().into()])),
|
||||
config,
|
||||
project,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_arc_str <'a> (_: &App, src: impl Language) -> Perhaps<Arc<str>> {
|
||||
Ok(src.src()?.map(|x|x.into()))
|
||||
}
|
||||
|
||||
fn get_u16 <'a> (state: &App, src: impl Language) -> Perhaps<u16> {
|
||||
Ok(Some(match src.word()? {
|
||||
Some(":w/sidebar") => state.project.w_sidebar(state.editor().is_some()),
|
||||
Some(":h/sample-detail") => 6.max(state.size.h() as u16 * 3 / 9),
|
||||
_ => return try_to_u16(src)
|
||||
}))
|
||||
}
|
||||
|
||||
fn get_usize <'a> (state: &App, src: impl Language) -> Perhaps<usize> {
|
||||
Ok(Some(match src.word()? {
|
||||
Some(":scene-count") => state.scenes().len(),
|
||||
Some(":track-count") => state.tracks().len(),
|
||||
Some(":device-kind") => state.dialog.device_kind().unwrap_or(0),
|
||||
Some(":device-kind/next") => state.dialog.device_kind_next().unwrap_or(0),
|
||||
Some(":device-kind/prev") => state.dialog.device_kind_prev().unwrap_or(0),
|
||||
_ => return try_to_usize(src)
|
||||
}))
|
||||
}
|
||||
|
||||
fn get_f64 <'a> (_state: &App, _src: impl Language) -> Perhaps<f64> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn get_bool <'a> (state: &App, src: impl Language) -> Perhaps<bool> {
|
||||
src.word()?.map(|word|Ok(match word {
|
||||
"Y" => true,
|
||||
"N" => false,
|
||||
":mode/editor" => state.project.editor.is_some(),
|
||||
":focused/dialog" => !matches!(state.dialog, Dialog::None),
|
||||
":focused/message" => matches!(state.dialog, Dialog::Message(..)),
|
||||
":focused/add_device" => matches!(state.dialog, Dialog::Device(..)),
|
||||
":focused/browser" => state.dialog.browser().is_some(),
|
||||
":focused/pool/import" => matches!(state.project.pool.mode, Some(PoolMode::Import(..))),
|
||||
":focused/pool/export" => matches!(state.project.pool.mode, Some(PoolMode::Export(..))),
|
||||
":focused/pool/rename" => matches!(state.project.pool.mode, Some(PoolMode::Rename(..))),
|
||||
":focused/pool/length" => matches!(state.project.pool.mode, Some(PoolMode::Length(..))),
|
||||
":focused/clip" => !state.editor_focused() && matches!(state.selection(), Selection::TrackClip{..}),
|
||||
":focused/track" => !state.editor_focused() && matches!(state.selection(), Selection::Track(..)),
|
||||
":focused/scene" => !state.editor_focused() && matches!(state.selection(), Selection::Scene(..)),
|
||||
":focused/mix" => !state.editor_focused() && matches!(state.selection(), Selection::Mix),
|
||||
_ => return err!("not bool: {word}")
|
||||
})).transpose()
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
fn get_selection <'a> (state: &App, src: impl Language) -> Perhaps<Selection> {
|
||||
src.word()?.map(|word|Ok(match word {
|
||||
":select/scene" => state.selection().select_scene(state.tracks().len()),
|
||||
":select/scene/next" => state.selection().select_scene_next(state.scenes().len()),
|
||||
":select/scene/prev" => state.selection().select_scene_prev(),
|
||||
":select/track" => state.selection().select_track(state.tracks().len()),
|
||||
":select/track/next" => state.selection().select_track_next(state.tracks().len()),
|
||||
":select/track/prev" => state.selection().select_track_prev(),
|
||||
_ => return err!("not selection: {word}")
|
||||
})).transpose()
|
||||
}
|
||||
|
||||
fn get_color <'a> (_: &App, src: impl Language) -> Perhaps<Color> {
|
||||
if let Some(expr) = src.expr()? {
|
||||
match (expr.head()?, expr.tail()?) {
|
||||
(Some("g"), Some(tail)) => {
|
||||
let n = try_to_u8(tail.head().map_err(Into::into))?
|
||||
.ok_or(LanguageError::domain("not gray"))?;
|
||||
Ok(Some(Color::Rgb(n, n, n)))
|
||||
},
|
||||
(Some("rgb"), Some(tail)) => {
|
||||
let r = try_to_u8(tail.head().map_err(Into::into))?
|
||||
.ok_or(LanguageError::domain("not red"))?;
|
||||
let g = try_to_u8(tail.tail().head().map_err(Into::into))?
|
||||
.ok_or(LanguageError::domain("not green"))?;
|
||||
let b = try_to_u8(tail.tail().tail().head().map_err(Into::into))?
|
||||
.ok_or(LanguageError::domain("not blue"))?;
|
||||
Ok(Some(Color::Rgb(r, g, b)))
|
||||
},
|
||||
(Some(_), _) => return err!("not a color expression: {expr}"),
|
||||
(None, _) => return err!("not a color expression: {expr}"),
|
||||
}
|
||||
} else if let Ok(Some(sym)) = src.word() {
|
||||
Ok(match sym {
|
||||
":color/bg" => Some(Color::Rgb(28, 32, 36)),
|
||||
":color/fg" => Some(Color::Rgb(98, 92, 96)),
|
||||
_ => return err!("not a color: {sym}")
|
||||
})
|
||||
} else {
|
||||
return err!("not a color: {:?}", src.src()?)
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
fn get_opt_u7 <'a> (state: &App, src: impl Language) -> Perhaps<Option<u7>> {
|
||||
eval_word(src, |word|Ok(match word {
|
||||
|
||||
":editor/pitch" => Some((
|
||||
state.editor().as_ref().map(|e|e.get_note_pos()).unwrap() as u8
|
||||
).into()),
|
||||
|
||||
_ => return err!("unknown midi note: {word}")
|
||||
|
||||
}))
|
||||
}
|
||||
|
||||
fn get_opt_usize <'a> (state: &App, src: impl Language) -> Perhaps<Option<usize>> {
|
||||
eval_word(src, |word|Ok(match word {
|
||||
|
||||
":selected/scene" => state.selection().scene(),
|
||||
":selected/track" => state.selection().track(),
|
||||
|
||||
_ => return err!("unknown opt<usize>: {word}")
|
||||
|
||||
}))
|
||||
}
|
||||
|
||||
fn get_axis <'a> (_: &App, src: impl Language) -> Perhaps<ControlAxis> {
|
||||
eval_word(src, |word|Ok(match word {
|
||||
|
||||
"X" => ControlAxis::X, "XFine" | "X2" => ControlAxis::XFine,
|
||||
"Y" => ControlAxis::Y, "YFine" | "Y2" => ControlAxis::YFine,
|
||||
"Z" => ControlAxis::Z, "ZFine" | "Z2" => ControlAxis::ZFine,
|
||||
|
||||
_ => return err!("unknown axis {word}")
|
||||
|
||||
}))
|
||||
}
|
||||
|
||||
fn get_opt_u16 <'a> (_: &App, _src: impl Language) -> Perhaps<Option<u16>> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn get_opt_u32 <'a> (_: &App, _src: impl Language) -> Perhaps<Option<u32>> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn get_opt_itemtheme <'a> (_: &App, _: impl Language) -> Perhaps<Option<ItemTheme>> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn get_opt_arc_array_connect <'a> (_: &App, _: impl Language) -> Perhaps<Option<Arc<[Connect]>>> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn get_arc_array_connect <'a> (_: &App, _: impl Language) -> Perhaps<Arc<[Connect]>> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn get_opt_arc_str <'a> (_: &App, _: impl Language) -> Perhaps<Option<Arc<str>>> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn get_opt_bool <'a> (_: &App, _: impl Language) -> Perhaps<Option<bool>> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn get_path_buf <'a> (_: &App, _: impl Language) -> Perhaps<PathBuf> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn get_vec_arc_str <'a> (_: &App, _: impl Language) -> Perhaps<Vec<Arc<str>>> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
impl_has!(Clock: |self: App|self.project.clock);
|
||||
impl_has!(Dialog: |self: App|self.dialog);
|
||||
impl_has!(Jack<'static>: |self: App|self.jack);
|
||||
impl_has!(Pool: |self: App|self.project.pool);
|
||||
impl_has!(Selection: |self: App|self.project.selection);
|
||||
impl_as_ref!(Option<MidiEditor>: |self: App|self.project.as_ref());
|
||||
impl_as_mut!(Option<MidiEditor>: |self: App|self.project.as_mut());
|
||||
|
||||
primitive!(u8: try_to_u8);
|
||||
primitive!(u16: try_to_u16);
|
||||
primitive!(usize: try_to_usize);
|
||||
primitive!(isize: try_to_isize);
|
||||
|
||||
impl HasClips for App {
|
||||
fn clip (&self) -> Option<Arc<RwLock<MidiClip>>> {
|
||||
self.project.clip()
|
||||
}
|
||||
fn clip_mut (&self) -> &mut Option<Arc<RwLock<MidiClip>>> {
|
||||
self.project.clip_mut()
|
||||
}
|
||||
fn clips <'a> (&'a self) -> RwLockReadGuard<'a, Clips> {
|
||||
self.project.clips()
|
||||
}
|
||||
fn clips_mut <'a> (&'a self) -> RwLockWriteGuard<'a, Clips> {
|
||||
self.project.clips_mut()
|
||||
}
|
||||
}
|
||||
|
||||
tui_keys!(self: App, input {
|
||||
#[cfg(feature = "prof2")] profiling::scope!("App::tui_keys!");
|
||||
Self::input_cmd(self, input)
|
||||
});
|
||||
|
||||
impl App {
|
||||
|
||||
fn input_cmd (state: &mut Self, input: &TuiEvent) -> Usually<()> {
|
||||
if let Some(bindings) = Self::input_cmd_find(state, input) {
|
||||
return Self::input_cmd_exec(state, bindings)
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn input_cmd_find (state: &mut Self, input: &TuiEvent) -> Option<Arc<[Binding<Arc<str>>]>> {
|
||||
for name in state.modes.read().iter().rev() {
|
||||
let mode = state.config.get_mode(name).as_ref().map(Arc::clone);
|
||||
if let Some(mode) = mode {
|
||||
let binds = state.config.binds.clone();
|
||||
for id in mode.keys.iter() {
|
||||
if let Some(event_map) = binds.find(id)
|
||||
&& let Some(bindings) = event_map.query(input) {
|
||||
return Some(bindings.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn input_cmd_exec (state: &mut Self, bindings: Arc<[Binding<Arc<str>>]>) -> Usually<()> {
|
||||
for binding in bindings.iter() {
|
||||
for cmd in binding.commands.iter() {
|
||||
if Self::cmd::<AppCmd>(state, cmd)? {
|
||||
return Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cmd <
|
||||
T: Dispatch<Self> + Clone + Into<AppCmd>
|
||||
> (state: &mut Self, src: &str) -> Usually<bool> where
|
||||
Self: Namespace<T>,
|
||||
{
|
||||
if let Some(cmd) = Namespace::<T>::namespace(state, &src)? {
|
||||
return Self::cmd_dispatch(state, cmd.into())
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
fn cmd_dispatch (state: &mut Self, cmd: AppCmd) -> Usually<bool> where
|
||||
Self: Namespace<AppCmd>,
|
||||
{
|
||||
match cmd.clone().dispatch(state) {
|
||||
Err(err) => {
|
||||
state.history.push((cmd.into(), None));
|
||||
return Err(err)
|
||||
},
|
||||
Ok(undo) => {
|
||||
state.history.push((cmd.into(), undo.map(Into::into)));
|
||||
return Ok(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A control axis.
|
||||
///
|
||||
/// ```
|
||||
/// let axis = tek::ControlAxis::X;
|
||||
/// ```
|
||||
#[derive(Debug, Copy, Clone)] pub enum ControlAxis {
|
||||
X, XFine,
|
||||
Y, YFine,
|
||||
Z, ZFine,
|
||||
I, IFine,
|
||||
}
|
||||
|
||||
/// The [Draw] implementation for [App] handles the loaded view,
|
||||
/// which is defined in terms of [dizzle] DSL.
|
||||
///
|
||||
/// If there is an error, the error is displayed. FIXME: overlay it
|
||||
/// Then, every top-level form of the DSL description is rendered.
|
||||
impl Draw<Tui> for App {
|
||||
fn draw (&self, to: &mut Tui) -> Drawn<u16> {
|
||||
#[cfg(feature = "prof2")] profiling::scope!("App::draw");
|
||||
//self.perf.cycle(&mut |_|{
|
||||
self.draw_error(to)?;
|
||||
self.draw_modes(to)?;
|
||||
self.draw_debug(to)?;
|
||||
#[cfg(feature = "prof2")] profiling::finish_frame!();
|
||||
Ok(Some(to.area().into()))
|
||||
//})
|
||||
}
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn draw_error (&self, to: &mut Tui) -> Usually<()> {
|
||||
if let Some(e) = self.error.try_read().unwrap().as_ref() {
|
||||
e.as_ref().align_c().draw(to)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw_modes (&self, to: &mut Tui) -> Usually<()> {
|
||||
for name in self.modes.read().iter() {
|
||||
let mut error = false;
|
||||
if let Some(mode) = self.config.get_mode(name).as_ref().map(Arc::clone) {
|
||||
for (index, view) in mode.view.iter().enumerate() {
|
||||
match (view.render)(self, to) {
|
||||
Ok(None) => {},
|
||||
Ok(Some(XYWH(.., w, h))) => {
|
||||
self.size.0.store(w as usize, Relaxed);
|
||||
self.size.1.store(h as usize, Relaxed);
|
||||
},
|
||||
Err(e) => {
|
||||
let message = format!(
|
||||
"Mode: {:?}\n\nLayer: #{index}\n\nError: {e}\n\nSource:\n{}",
|
||||
&mode.name,
|
||||
&view.source
|
||||
);
|
||||
*self.error.try_write().unwrap() = Some(message.into());
|
||||
error = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if error {
|
||||
break
|
||||
} else {
|
||||
*self.error.try_write().unwrap() = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(unused)] fn draw_debug (&self, to: &mut Tui) -> Drawn<u16> {
|
||||
Split::Above.bar(0,
|
||||
Split::West.bar(16,
|
||||
format!("{}/{} {} ",
|
||||
self.perf.used.load(Relaxed),
|
||||
self.perf.window.load(Relaxed),
|
||||
self.perf.clock.raw() / 10000000).align_e(),
|
||||
format!("{}x{} ",
|
||||
self.size.0.load(Relaxed),
|
||||
self.size.1.load(Relaxed)).align_w(),
|
||||
).align_se(),
|
||||
Split::East.bar(16,
|
||||
format!("{}", self.modes.read()[0]),
|
||||
format!("{}", self.modes.read()[0])
|
||||
).align_sw()
|
||||
).draw(to)
|
||||
}
|
||||
}
|
||||
|
||||
impl_has!(Sizer: |self: App|self.size);
|
||||
|
||||
pub trait HasWidth {
|
||||
const MIN_WIDTH: usize;
|
||||
/// Increment track width.
|
||||
fn width_inc (&mut self);
|
||||
/// Decrement track width, down to a hardcoded minimum of [Self::MIN_WIDTH].
|
||||
fn width_dec (&mut self);
|
||||
}
|
||||
|
||||
pub fn get_dialog <'a> (_: &App, src: impl Language) -> Perhaps<Dialog> {
|
||||
|
||||
eval_word(&src, |word|Ok(match word {
|
||||
|
||||
":none" => Dialog::None,
|
||||
|
||||
":options" => Dialog::Options,
|
||||
|
||||
":device" => Dialog::Device(0),
|
||||
":device/prev" => Dialog::Device(0),
|
||||
":device/next" => Dialog::Device(0),
|
||||
|
||||
":help" => Dialog::Help(0),
|
||||
":save" => Dialog::Browse(
|
||||
BrowseTarget::SaveProject,
|
||||
Browse::new(None).unwrap().into()),
|
||||
":load" => Dialog::Browse(
|
||||
BrowseTarget::LoadProject,
|
||||
Browse::new(None).unwrap().into()),
|
||||
|
||||
":import/clip" => Dialog::Browse(
|
||||
BrowseTarget::ImportClip(Default::default()),
|
||||
Browse::new(None).unwrap().into()),
|
||||
":export/clip" => Dialog::Browse(
|
||||
BrowseTarget::ExportClip(Default::default()),
|
||||
Browse::new(None).unwrap().into()),
|
||||
|
||||
":import/sample" => Dialog::Browse(
|
||||
BrowseTarget::ImportSample(Default::default()),
|
||||
Browse::new(None).unwrap().into()),
|
||||
":export/sample" => Dialog::Browse(
|
||||
BrowseTarget::ExportSample(Default::default()),
|
||||
Browse::new(None).unwrap().into()),
|
||||
|
||||
_ => return Err(format!("not a dialog: {}", src.src()?.unwrap_or("<???>")).into())
|
||||
|
||||
}))
|
||||
|
||||
}
|
||||
|
||||
/// Various possible dialog modes.
|
||||
///
|
||||
/// ```
|
||||
/// let dialog: tek::Dialog = Default::default();
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Default, PartialEq)] pub enum Dialog {
|
||||
#[default] None,
|
||||
Help(usize),
|
||||
Menu(usize, Arc<[MenuItem]>),
|
||||
Device(usize),
|
||||
Message(Arc<str>),
|
||||
Browse(BrowseTarget, Arc<Browse>),
|
||||
Options,
|
||||
}
|
||||
|
||||
impl Dialog {
|
||||
|
||||
/// ```
|
||||
/// let _ = tek::Dialog::welcome();
|
||||
/// ```
|
||||
pub fn welcome () -> Self {
|
||||
Self::Menu(1, Arc::new([
|
||||
|
||||
MenuItem::new(" Resume session ", [
|
||||
AppCmd::Project {
|
||||
command: ProjectCmd::LoadLast
|
||||
}
|
||||
]),
|
||||
|
||||
MenuItem::new(" Create session ", [
|
||||
AppCmd::Dialog {
|
||||
dialog: Dialog::None
|
||||
},
|
||||
AppCmd::Mode {
|
||||
command: ModeCmd::Reset {
|
||||
modes: vec!["Arranger".into()]
|
||||
}
|
||||
},
|
||||
AppCmd::Tracks {
|
||||
command: TracksCmd::Add
|
||||
},
|
||||
AppCmd::Scenes {
|
||||
command: ScenesCmd::Add
|
||||
}
|
||||
]),
|
||||
|
||||
MenuItem::new(" Load session ", [
|
||||
AppCmd::Dialog {
|
||||
dialog: Dialog::Browse(
|
||||
BrowseTarget::LoadProject,
|
||||
Browse::new(None).unwrap().into()
|
||||
)
|
||||
//dialog: Dialog::Browse {
|
||||
//title: "Load session".into(),
|
||||
//filter: "\\.tek$",
|
||||
//}
|
||||
}
|
||||
]),
|
||||
|
||||
MenuItem::new(" Exit ", [
|
||||
AppCmd::Exit
|
||||
]),
|
||||
|
||||
]))
|
||||
}
|
||||
|
||||
/// FIXME: generalize
|
||||
/// ```
|
||||
/// let _ = tek::Dialog::welcome().menu_selected();
|
||||
/// ```
|
||||
pub fn menu_selected (&self) -> Option<usize> {
|
||||
if let Self::Menu(selected, _) = self { Some(*selected) } else { None }
|
||||
}
|
||||
|
||||
/// FIXME: generalize
|
||||
/// ```
|
||||
/// let _ = tek::Dialog::welcome().menu_next();
|
||||
/// ```
|
||||
pub fn menu_next (&self) -> Self {
|
||||
match self {
|
||||
Self::Menu(index, items) => Self::Menu(wrap_inc(*index, items.len()), items.clone()),
|
||||
_ => Self::None
|
||||
}
|
||||
}
|
||||
|
||||
/// FIXME: generalize
|
||||
/// ```
|
||||
/// let _ = tek::Dialog::welcome().menu_prev();
|
||||
/// ```
|
||||
pub fn menu_prev (&self) -> Self {
|
||||
match self {
|
||||
Self::Menu(index, items) => Self::Menu(wrap_dec(*index, items.len()), items.clone()),
|
||||
_ => Self::None
|
||||
}
|
||||
}
|
||||
|
||||
/// FIXME: generalize
|
||||
/// ```
|
||||
/// let _ = tek::Dialog::welcome().device_kind();
|
||||
/// ```
|
||||
pub fn device_kind (&self) -> Option<usize> {
|
||||
if let Self::Device(index) = self { Some(*index) } else { None }
|
||||
}
|
||||
|
||||
/// FIXME: generalize
|
||||
/// ```
|
||||
/// let _ = tek::Dialog::welcome().device_kind_next();
|
||||
/// ```
|
||||
pub fn device_kind_next (&self) -> Option<usize> {
|
||||
self.device_kind().map(|index|(index + 1) % device_kinds().len())
|
||||
}
|
||||
|
||||
/// FIXME: generalize
|
||||
/// ```
|
||||
/// let _ = tek::Dialog::welcome().device_kind_prev();
|
||||
/// ```
|
||||
pub fn device_kind_prev (&self) -> Option<usize> {
|
||||
self.device_kind().map(|index|index.overflowing_sub(1).0.min(device_kinds().len().saturating_sub(1)))
|
||||
}
|
||||
|
||||
/// FIXME: implement
|
||||
pub fn message (&self) -> Option<&str> { todo!() }
|
||||
|
||||
/// FIXME: implement
|
||||
pub fn browser (&self) -> Option<&Arc<Browse>> { todo!() }
|
||||
|
||||
/// FIXME: implement
|
||||
pub fn browser_target (&self) -> Option<&BrowseTarget> { todo!() }
|
||||
|
||||
}
|
||||
|
||||
/// Increment a wrapping counter.
|
||||
pub const fn wrap_inc (index: usize, count: usize) -> usize {
|
||||
if count > 0 { (index + 1) % count } else { 0 }
|
||||
}
|
||||
|
||||
/// Decrement a wrapping counter.
|
||||
pub const fn wrap_dec (index: usize, count: usize) -> usize {
|
||||
if count > 0 {
|
||||
let a = index.overflowing_sub(1).0;
|
||||
let b = count.saturating_sub(1);
|
||||
if a < b { a } else { b }
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
/// An item of a menu.
|
||||
///
|
||||
/// ```
|
||||
/// let item: tek::MenuItem = Default::default();
|
||||
/// ```
|
||||
#[derive(Clone)] pub struct MenuItem(
|
||||
pub Arc<str>,
|
||||
pub Arc<[AppCmd]>,
|
||||
);
|
||||
|
||||
impl MenuItem {
|
||||
pub fn new <const N: usize> (name: impl AsRef<str>, actions: [AppCmd; N]) -> Self {
|
||||
Self(name.as_ref().into(), Arc::new(actions))
|
||||
}
|
||||
}
|
||||
|
||||
impl_debug!(MenuItem |self, w| { write!(w, "{}", &self.0) });
|
||||
|
||||
impl_default!(MenuItem: Self::new("", []));
|
||||
|
||||
impl PartialEq for MenuItem {
|
||||
fn eq (&self, other: &Self) -> bool {
|
||||
self.0 == other.0
|
||||
}
|
||||
}
|
||||
|
||||
//impl AsRef<Arc<[MenuItem]>> for MenuItems {
|
||||
//fn as_ref (&self) -> &Arc<[MenuItem]> {
|
||||
//&self.0
|
||||
//}
|
||||
//}
|
||||
|
|
|
|||
|
|
@ -1,50 +1,5 @@
|
|||
use crate::*;
|
||||
|
||||
impl App {
|
||||
/// Return reference to content browser if open.
|
||||
///
|
||||
/// ```
|
||||
/// assert_eq!(tek::App::default().browser(), None);
|
||||
/// ```
|
||||
pub fn browser (&self) -> Option<&Browse> {
|
||||
if let Dialog::Browse(_, ref b) = self.dialog { Some(b) } else { None }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T:
|
||||
Namespace<usize> +
|
||||
Namespace<PathBuf> +
|
||||
Namespace<Arc<str>>
|
||||
> BrowseController for T {}
|
||||
|
||||
#[tek_proc::commands(BrowseCommand = "browse")]
|
||||
pub trait BrowseController:
|
||||
Namespace<usize> +
|
||||
Namespace<PathBuf> +
|
||||
Namespace<Arc<str>>
|
||||
{
|
||||
/// Toggle visibility of browser
|
||||
#[command(Show = "show")]
|
||||
fn show (&mut self) -> Perhaps<BrowseCommand> {
|
||||
todo!()
|
||||
}
|
||||
/// Set current directory
|
||||
#[command(Chdir = "chdir")]
|
||||
fn chdir (&mut self, _path: PathBuf) -> Perhaps<BrowseCommand> {
|
||||
todo!()
|
||||
}
|
||||
/// Set current filter
|
||||
#[command(SetSearch = "set-search")]
|
||||
fn set_search (&mut self, _filter: Arc<str>) -> Perhaps<BrowseCommand> {
|
||||
todo!()
|
||||
}
|
||||
/// Set selected item
|
||||
#[command(SetCursor = "set-cursor")]
|
||||
fn set_cursor (&mut self, _index: usize) -> Perhaps<BrowseCommand> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
/// Browses for files to load/save.
|
||||
///
|
||||
/// ```
|
||||
|
|
@ -179,13 +134,13 @@ pub fn scan (dir: &PathBuf) -> Usually<(Vec<OsString>, Vec<OsString>)> {
|
|||
Ok((subdirs, files))
|
||||
}
|
||||
|
||||
pub fn view_browse_title (state: &App) -> impl Draw<Tui> {
|
||||
field_v(ItemTheme::default(), match state.dialog.browser_target().unwrap() {
|
||||
BrowseTarget::SaveProject => "Save project:",
|
||||
BrowseTarget::LoadProject => "Load project:",
|
||||
BrowseTarget::ImportSample(_) => "Import sample:",
|
||||
BrowseTarget::ExportSample(_) => "Export sample:",
|
||||
BrowseTarget::ImportClip(_) => "Import clip:",
|
||||
BrowseTarget::ExportClip(_) => "Export clip:",
|
||||
}, fg(g(96), x_repeat("🭻")).exact_h(1)).align_w().full_w()
|
||||
impl App {
|
||||
/// Return reference to content browser if open.
|
||||
///
|
||||
/// ```
|
||||
/// assert_eq!(tek::App::default().browser(), None);
|
||||
/// ```
|
||||
pub fn browser (&self) -> Option<&Browse> {
|
||||
if let Dialog::Browse(_, ref b) = self.dialog { Some(b) } else { None }
|
||||
}
|
||||
}
|
||||
822
src/tek_ctrl.rs
Normal file
822
src/tek_ctrl.rs
Normal file
|
|
@ -0,0 +1,822 @@
|
|||
use crate::*;
|
||||
|
||||
impl_from!(AppCmd: |x: AxisCmd| AppCmd::Axis { command: x });
|
||||
impl_from!(AppCmd: |x: BrowseCmd| AppCmd::Browse { command: x });
|
||||
impl_from!(AppCmd: |x: MidiClipCmd| AppCmd::MidiClip { command: x });
|
||||
impl_from!(AppCmd: |x: MidiEditCmd| AppCmd::MidiEdit { command: x });
|
||||
impl_from!(AppCmd: |x: ModeCmd| AppCmd::Mode { command: x });
|
||||
impl_from!(AppCmd: |x: PoolCmd| AppCmd::Pool { command: x });
|
||||
impl_from!(AppCmd: |x: ProjectCmd| AppCmd::Project { command: x });
|
||||
impl_from!(AppCmd: |x: SceneCmd| AppCmd::Scene { command: x });
|
||||
impl_from!(AppCmd: |x: ScenesCmd| AppCmd::Scenes { command: x });
|
||||
impl_from!(AppCmd: |x: TrackCmd| AppCmd::Track { command: x });
|
||||
impl_from!(AppCmd: |x: TracksCmd| AppCmd::Tracks { command: x });
|
||||
|
||||
#[tek_proc::commands(AppCmd)] impl App {
|
||||
/// Exit the application.
|
||||
#[command(Exit = "exit")] pub fn cmd_exit (&mut self) -> Perhaps<AppCmd> {
|
||||
self.exit.exit();
|
||||
Ok(None)
|
||||
}
|
||||
/// Cancel current selection.
|
||||
#[command(Cancel = "cancel")] pub fn cmd_cancel (&mut self) -> Perhaps<AppCmd> {
|
||||
todo!("cancel")
|
||||
}
|
||||
/// Confirm current selection.
|
||||
#[command(Confirm = "confirm")] pub fn cmd_confirm (&mut self) -> Perhaps<AppCmd> {
|
||||
Ok(match self.dialog.clone() {
|
||||
Dialog::Menu(index, items) => {
|
||||
for command in items[index].1.iter() {
|
||||
App::cmd_dispatch(self, command.clone())?;
|
||||
}
|
||||
None
|
||||
}
|
||||
_ => todo!(),
|
||||
})
|
||||
}
|
||||
/// Swap currently active modal dialog.
|
||||
///
|
||||
/// ```
|
||||
/// let _ = tek::App::default().cmd_dialog(tek::Dialog::welcome());
|
||||
/// ```
|
||||
#[command(Dialog = "dialog")] pub fn cmd_dialog (&mut self, dialog: Dialog) -> Perhaps<AppCmd> {
|
||||
let mut dialog = dialog.clone();
|
||||
std::mem::swap(&mut self.dialog, &mut dialog);
|
||||
Ok(Some(AppCmd::Dialog { dialog }))
|
||||
}
|
||||
#[command(Mode = "mode")] pub fn cmd_mode (&mut self, command: ModeCmd) -> Perhaps<AppCmd> {
|
||||
Ok(command.dispatch(self)?.map(Into::into))
|
||||
}
|
||||
#[command(Project = "session")] pub fn cmd_session (&mut self, command: ProjectCmd) -> Perhaps<AppCmd> {
|
||||
Ok(command.dispatch(self)?.map(Into::into))
|
||||
}
|
||||
#[command(Axis = "axis")] pub fn cmd_axis (&mut self, command: AxisCmd) -> Perhaps<AppCmd> {
|
||||
Ok(command.dispatch(self)?.map(Into::into))
|
||||
}
|
||||
#[command(Browse = "browse")] pub fn cmd_browse (&mut self, command: BrowseCmd) -> Perhaps<AppCmd> {
|
||||
Ok(command.dispatch(self)?.map(Into::into))
|
||||
}
|
||||
#[command(MidiClip = "clip")] pub fn cmd_midi_clip (&mut self, command: MidiClipCmd) -> Perhaps<AppCmd> {
|
||||
Ok(command.dispatch(self)?.map(Into::into))
|
||||
}
|
||||
#[command(MidiEdit = "edit")] pub fn cmd_midi_edit (&mut self, command: MidiEditCmd) -> Perhaps<AppCmd> {
|
||||
Ok(command.dispatch(self)?.map(Into::into))
|
||||
}
|
||||
#[command(Pool = "pool")] pub fn cmd_pool (&mut self, command: PoolCmd) -> Perhaps<AppCmd> {
|
||||
Ok(command.dispatch(self)?.map(Into::into))
|
||||
}
|
||||
#[command(Scene = "scene")] pub fn cmd_scene (&mut self, command: SceneCmd) -> Perhaps<AppCmd> {
|
||||
Ok(command.dispatch(self)?.map(Into::into))
|
||||
}
|
||||
#[command(Scenes = "scenes")] pub fn cmd_scenes (&mut self, command: ScenesCmd) -> Perhaps<AppCmd> {
|
||||
Ok(command.dispatch(self)?.map(Into::into))
|
||||
}
|
||||
#[command(Track = "track")] pub fn cmd_track (&mut self, command: TrackCmd) -> Perhaps<AppCmd> {
|
||||
Ok(command.dispatch(self)?.map(Into::into))
|
||||
}
|
||||
#[command(Tracks = "tracks")] pub fn cmd_tracks (&mut self, command: TracksCmd) -> Perhaps<AppCmd> {
|
||||
Ok(command.dispatch(self)?.map(Into::into))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: HasProject> ProjectControl for T {}
|
||||
|
||||
#[tek_proc::commands(ProjectCmd)] pub trait ProjectControl: HasProject {
|
||||
#[command(LoadLast = "load-last")] fn load_last (&mut self) -> Perhaps<ProjectCmd> {
|
||||
todo!()
|
||||
}
|
||||
#[command(Load = "load")] fn load (&mut self) -> Perhaps<ProjectCmd> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: HasClock + Namespace<u32> + Namespace<f64> + Namespace<Option<u32>>> ClockControl for T {}
|
||||
|
||||
#[tek_proc::commands(ClockCmd = "clock")] pub trait ClockControl: HasClock
|
||||
+ Namespace<u32>
|
||||
+ Namespace<f64>
|
||||
+ Namespace<Option<u32>>
|
||||
{
|
||||
#[command(SeekUsec = "usec")] fn seek_usec (&mut self, usec: f64) -> Perhaps<ClockCmd> {
|
||||
self.clock().playhead.update_from_usec(usec);
|
||||
Ok(None)
|
||||
}
|
||||
#[command(SeekSample = "sample")] fn seek_sample (&mut self, sample: f64) -> Perhaps<ClockCmd> {
|
||||
self.clock().playhead.update_from_sample(sample);
|
||||
Ok(None)
|
||||
}
|
||||
#[command(SeekPulse = "pulse")] fn seek_pulse (&mut self, pulse: f64) -> Perhaps<ClockCmd> {
|
||||
self.clock().playhead.update_from_pulse(pulse);
|
||||
Ok(None)
|
||||
}
|
||||
#[command(SetBpm = "bpm")] fn set_bpm (&mut self, bpm: f64) -> Perhaps<ClockCmd> {
|
||||
Ok(Some(ClockCmd::SetBpm {
|
||||
bpm: self.clock().timebase().bpm.set(bpm)
|
||||
}))
|
||||
}
|
||||
#[command(SetQuant = "quant")] fn set_quant (&mut self, quant: f64) -> Perhaps<ClockCmd> {
|
||||
Ok(Some(ClockCmd::SetQuant {
|
||||
quant: self.clock().quant.set(quant)
|
||||
}))
|
||||
}
|
||||
#[command(SetSync = "sync")] fn set_sync (&mut self, sync: f64) -> Perhaps<ClockCmd> {
|
||||
Ok(Some(ClockCmd::SetSync {
|
||||
sync: self.clock().sync.set(sync)
|
||||
}))
|
||||
}
|
||||
#[command(Play = "play")] fn play (&mut self, position: Option<u32>) -> Perhaps<ClockCmd> {
|
||||
self.clock().play_from(position)?;
|
||||
Ok(None) /* TODO Some(Pause(previousPosition)) */
|
||||
}
|
||||
#[command(Pause = "pause")] fn pause (&mut self, position: Option<u32>) -> Perhaps<ClockCmd> {
|
||||
self.clock().pause_at(position)?;
|
||||
Ok(None)
|
||||
}
|
||||
#[command(TogglePlayback = "toggle")] fn toggle (&mut self, position: u32) -> Perhaps<ClockCmd> {
|
||||
Ok(if self.clock().is_rolling() {
|
||||
self.clock().pause_at(Some(position))?;
|
||||
None
|
||||
} else {
|
||||
self.clock().play_from(Some(position))?;
|
||||
None
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: HasTracks
|
||||
+ HasScenes
|
||||
+ HasSelection
|
||||
+ HasJack<'static>
|
||||
+ Namespace<usize>
|
||||
+ Namespace<Arc<[Connect]>>
|
||||
+ Namespace<Option<usize>>
|
||||
+ Namespace<Option<Arc<str>>>
|
||||
+ Namespace<Option<ItemTheme>>
|
||||
+ Namespace<Option<Arc<[Connect]>>>
|
||||
+ Namespace<Option<Vec<Option<Arc<RwLock<MidiClip>>>>>>
|
||||
> TracksControl for T {}
|
||||
|
||||
#[tek_proc::commands(TracksCmd)] pub trait TracksControl: HasTracks
|
||||
+ HasScenes
|
||||
+ HasSelection
|
||||
+ HasJack<'static>
|
||||
+ Namespace<usize>
|
||||
+ Namespace<Arc<[Connect]>>
|
||||
+ Namespace<Option<usize>>
|
||||
+ Namespace<Option<Arc<str>>>
|
||||
+ Namespace<Option<ItemTheme>>
|
||||
+ Namespace<Option<Arc<[Connect]>>>
|
||||
+ Namespace<Option<Vec<Option<Arc<RwLock<MidiClip>>>>>>
|
||||
{
|
||||
/// Stop all playing clips
|
||||
#[command(Stop = "stop")] fn tracks_stop_all (&mut self) -> Perhaps<TracksCmd> {
|
||||
for track in self.tracks_mut().iter_mut() {
|
||||
track.sequencer.enqueue_next(None);
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
/// Launch multiple clips
|
||||
#[command(Launch = "launch")] fn tracks_launch (
|
||||
&mut self, clips: Option<Vec<Option<Arc<RwLock<MidiClip>>>>>
|
||||
) -> Perhaps<TracksCmd> {
|
||||
if let Some(clips) = clips {
|
||||
for (clip, track) in clips.iter().zip(self.tracks_mut()) {
|
||||
track.sequencer.enqueue_next(clip.as_ref());
|
||||
}
|
||||
} else {
|
||||
for track in self.tracks_mut().iter_mut() {
|
||||
track.sequencer.enqueue_next(None);
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
#[command(Add = "add")] fn tracks_add (&mut self) -> Perhaps<TracksCmd>
|
||||
where Self: HasScenes + HasJack<'static>
|
||||
{
|
||||
let (_index, _) = self.tracks_add_one(None, None, [].into(), [].into())?;
|
||||
Ok(None)
|
||||
}
|
||||
#[command(Select = "select")] fn track_select (&mut self) -> Perhaps<TracksCmd> {
|
||||
let tracks = self.tracks().len();
|
||||
let select = self.selection_mut().select_track(tracks);
|
||||
*self.selection_mut() = select;
|
||||
Ok(None)
|
||||
}
|
||||
#[command(Next = "next")] fn track_select_next (&mut self) -> Perhaps<TracksCmd> {
|
||||
let tracks = self.tracks().len();
|
||||
let select = self.selection().select_track_next(tracks);
|
||||
*self.selection_mut() = select;
|
||||
Ok(None)
|
||||
}
|
||||
#[command(Prev = "prev")] fn track_select_prev (&mut self) -> Perhaps<TracksCmd> {
|
||||
let select = self.selection_mut().select_track_prev();
|
||||
*self.selection_mut() = select;
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: HasTrack
|
||||
+ Namespace<usize>
|
||||
+ Namespace<Arc<str>>
|
||||
+ Namespace<ItemTheme>
|
||||
+ Namespace<Option<bool>>
|
||||
> TrackControl for T {}
|
||||
|
||||
#[tek_proc::commands(TrackCmd)] pub trait TrackControl: HasTrack
|
||||
+ Namespace<usize>
|
||||
+ Namespace<Arc<str>>
|
||||
+ Namespace<ItemTheme>
|
||||
+ Namespace<Option<bool>>
|
||||
{
|
||||
#[command(Stop = "track/stop")]
|
||||
fn track_stop (&mut self) -> Perhaps<TrackCmd> {
|
||||
self.track_mut().map(|track|track.sequencer.enqueue_next(None));
|
||||
Ok(None)
|
||||
}
|
||||
#[command(SetMute = "track/mute")]
|
||||
fn track_set_mute (&mut self, _mute: Option<bool>) -> Perhaps<TrackCmd> {
|
||||
todo!()
|
||||
}
|
||||
#[command(SetSolo = "track/solo")]
|
||||
fn track_set_solo (&mut self, _solo: Option<bool>) -> Perhaps<TrackCmd> {
|
||||
todo!()
|
||||
}
|
||||
#[command(SetSize = "track/size")]
|
||||
fn track_set_size (&mut self, _size: usize) -> Perhaps<TrackCmd> {
|
||||
todo!()
|
||||
}
|
||||
#[command(SetZoom = "track/zoom")]
|
||||
fn track_set_zoom (&mut self, _zoom: usize) -> Perhaps<TrackCmd> {
|
||||
todo!()
|
||||
}
|
||||
#[command(SetName = "track/name")]
|
||||
fn track_set_name (&mut self, name: Arc<str>) -> Perhaps<TrackCmd> {
|
||||
self.track_mut()
|
||||
.map(|track|swap_value(&mut track.name, &name, |name|TrackCmd::SetName { name }))
|
||||
.transpose()
|
||||
.map(Option::flatten)
|
||||
}
|
||||
#[command(SetColor = "track/color")]
|
||||
fn track_set_color (&mut self, color: ItemTheme) -> Perhaps<TrackCmd> {
|
||||
self.track_mut()
|
||||
.map(|track|swap_value(&mut track.color, &color, |color|TrackCmd::SetColor { color }))
|
||||
.transpose()
|
||||
.map(Option::flatten)
|
||||
}
|
||||
#[command(SetRec = "track/rec")]
|
||||
fn track_set_rec (&mut self, rec: Option<bool>) -> Perhaps<TrackCmd> {
|
||||
self.track_mut()
|
||||
.map(|track|toggle_bool(&mut track.sequencer.recording, &rec, |rec|TrackCmd::SetRec { rec }))
|
||||
.transpose()
|
||||
.map(Option::flatten)
|
||||
}
|
||||
#[command(SetMon = "track/mon")]
|
||||
fn track_set_mon (&mut self, mon: Option<bool>) -> Perhaps<TrackCmd> {
|
||||
self.track_mut()
|
||||
.map(|track|toggle_bool(&mut track.sequencer.monitoring, &mon, |mon|TrackCmd::SetMon { mon }))
|
||||
.transpose()
|
||||
.map(Option::flatten)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: HasScenes
|
||||
+ HasTracks
|
||||
+ HasTrackScroll
|
||||
+ HasSelection
|
||||
+ HasJack<'static>
|
||||
+ Namespace<usize>
|
||||
+ Namespace<Option<ItemTheme>>
|
||||
+ Namespace<Option<Arc<str>>>
|
||||
> ScenesControl for T {}
|
||||
|
||||
#[tek_proc::commands(ScenesCmd)] pub trait ScenesControl: HasScenes
|
||||
+ HasTracks
|
||||
+ HasTrackScroll
|
||||
+ HasSelection
|
||||
+ HasJack<'static>
|
||||
+ Namespace<usize>
|
||||
+ Namespace<Option<ItemTheme>>
|
||||
+ Namespace<Option<Arc<str>>>
|
||||
{
|
||||
#[command(Add = "add")] fn scenes_add (&mut self) -> Perhaps<ScenesCmd> {
|
||||
let (_index, _) = self.scenes_add_one(None, None)?;
|
||||
Ok(None)
|
||||
}
|
||||
#[command(Select = "select")] fn scene_select (&mut self) -> Perhaps<ScenesCmd> {
|
||||
let scenes = self.scenes().len();
|
||||
let select = self.selection_mut().select_scene(scenes);
|
||||
*self.selection_mut() = select;
|
||||
Ok(None)
|
||||
}
|
||||
#[command(Next = "next")] fn scene_select_next (&mut self) -> Perhaps<ScenesCmd> {
|
||||
let scenes = self.scenes().len();
|
||||
let select = self.selection().select_scene_next(scenes);
|
||||
*self.selection_mut() = select;
|
||||
Ok(None)
|
||||
}
|
||||
#[command(Prev = "prev")] fn scene_select_prev (&mut self) -> Perhaps<ScenesCmd> {
|
||||
let select = self.selection_mut().select_scene_prev();
|
||||
*self.selection_mut() = select;
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: HasScene
|
||||
+ Namespace<usize>
|
||||
+ Namespace<Arc<str>>
|
||||
+ Namespace<ItemTheme>
|
||||
> SceneControl for T {}
|
||||
|
||||
#[tek_proc::commands(SceneCmd)] pub trait SceneControl: HasScene
|
||||
+ Namespace<usize>
|
||||
+ Namespace<Arc<str>>
|
||||
+ Namespace<ItemTheme>
|
||||
{
|
||||
#[command(SetSize = "size")] fn scene_set_size (&mut self, _size: usize) -> Perhaps<SceneCmd> {
|
||||
todo!()
|
||||
}
|
||||
#[command(SetZoom = "zoom")] fn scene_set_zoom (&mut self, _size: usize) -> Perhaps<SceneCmd> {
|
||||
todo!()
|
||||
}
|
||||
#[command(SetName = "name")] fn scene_set_name (&mut self, name: Arc<str>) -> Perhaps<SceneCmd> {
|
||||
Ok(self.scene_mut().map(|scene|swap_value(
|
||||
&mut scene.name,
|
||||
&name,
|
||||
|name|SceneCmd::SetName { name }
|
||||
)).transpose()?.flatten())
|
||||
}
|
||||
#[command(SetColor = "color")] fn scene_set_color (&mut self, color: ItemTheme) -> Perhaps<SceneCmd> {
|
||||
Ok(self.scene_mut().map(|scene|swap_value(
|
||||
&mut scene.color,
|
||||
&color,
|
||||
|color|SceneCmd::SetColor { color }
|
||||
)).transpose()?.flatten())
|
||||
}
|
||||
}
|
||||
|
||||
#[tek_proc::commands(SamplerCmd = "sampler")] pub trait SamplerControl: HasSampler
|
||||
+ Namespace<usize>
|
||||
{
|
||||
|
||||
#[command(RecordToggle = "rec-toggle")]
|
||||
fn record_toggle (&mut self, slot: usize) -> Perhaps<SamplerCmd>
|
||||
where Self: Sized
|
||||
{
|
||||
let sampler = self.sampler_mut();
|
||||
let recording = sampler.recording.as_ref().map(|x|x.0);
|
||||
let _ = SamplerCmd::RecordFinish.dispatch(self)?;
|
||||
// autoslice: continue recording at next slot
|
||||
if recording != Some(slot) {
|
||||
SamplerCmd::RecordBegin { slot }.dispatch(self)
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
#[command(RecordBegin = "rec-begin")] fn record_begin (&mut self, slot: usize) -> Perhaps<SamplerCmd> {
|
||||
let sampler = self.sampler_mut();
|
||||
sampler.recording = Some((
|
||||
slot,
|
||||
Some(Arc::new(RwLock::new(Sample::new(
|
||||
"Sample", 0, 0, vec![vec![]; sampler.audio_ins.len()]
|
||||
))))
|
||||
));
|
||||
Ok(None)
|
||||
}
|
||||
#[command(RecordFinish = "rec-finish")] fn record_finish (&mut self) -> Perhaps<SamplerCmd> {
|
||||
let sampler = self.sampler_mut();
|
||||
let _prev_sample = sampler.recording.as_mut().map(|(index, sample)|{
|
||||
std::mem::swap(sample, &mut sampler.samples.0[*index]);
|
||||
sample
|
||||
}); // TODO: undo
|
||||
Ok(None)
|
||||
}
|
||||
#[command(RecordCancel = "rec-cancel")] fn record_cancel (&mut self) -> Perhaps<SamplerCmd> {
|
||||
self.sampler_mut().recording = None;
|
||||
Ok(None)
|
||||
}
|
||||
#[command(PlaySample = "sample-play")] fn sample_play (&mut self, slot: usize) -> Perhaps<SamplerCmd> {
|
||||
let sampler = self.sampler_mut();
|
||||
if let Some(ref sample) = sampler.samples.0[slot] {
|
||||
sampler.voices.try_write().unwrap().push(Sample::play(sample, 0, &u7::from(128)));
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
#[command(StopSample = "sample-stop")] fn sample_stop (&mut self, slot: usize) -> Perhaps<SamplerCmd> {
|
||||
todo!();
|
||||
//Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: HasEditor
|
||||
+ Namespace<u32>
|
||||
+ Namespace<f64>
|
||||
+ Namespace<bool>
|
||||
+ Namespace<usize>
|
||||
+ Namespace<Option<u32>>
|
||||
+ Namespace<Option<Arc<RwLock<MidiClip>>>>
|
||||
> MidiEditControl for T {}
|
||||
|
||||
#[tek_proc::commands(MidiEditCmd)] pub trait MidiEditControl: HasEditor
|
||||
+ Namespace<u32>
|
||||
+ Namespace<f64>
|
||||
+ Namespace<bool>
|
||||
+ Namespace<usize>
|
||||
+ Namespace<Option<u32>>
|
||||
+ Namespace<Option<Arc<RwLock<MidiClip>>>>
|
||||
{
|
||||
#[command(Toggle = "toggle")] fn edit_toggle (&mut self) -> Perhaps<MidiEditCmd> {
|
||||
if self.is_editing() {
|
||||
*self.editor_mut() = None;
|
||||
} else {
|
||||
todo!()
|
||||
}
|
||||
Ok(Some(MidiEditCmd::Toggle))
|
||||
}
|
||||
#[command(Show = "show")] fn edit_show (&mut self, clip: Option<Arc<RwLock<MidiClip>>>) -> Perhaps<MidiEditCmd> {
|
||||
Ok(self.editor_mut().as_mut().map(|mut editor|{
|
||||
editor.set_clip(clip.as_ref());
|
||||
editor.redraw();
|
||||
None
|
||||
}).flatten())
|
||||
}
|
||||
#[command(DeleteNote = "delete")] fn edit_note_delete (&mut self) -> Perhaps<MidiEditCmd> {
|
||||
Ok(self.editor_mut().as_mut().map(|editor|{
|
||||
editor.redraw();
|
||||
todo!()
|
||||
}).flatten())
|
||||
}
|
||||
#[command(AppendNote = "append")] fn edit_note_append (&mut self, advance: bool) -> Perhaps<MidiEditCmd> {
|
||||
Ok(self.editor_mut().as_mut().map(|mut editor|{
|
||||
editor.put_note(advance);
|
||||
editor.redraw();
|
||||
None
|
||||
}).flatten())
|
||||
}
|
||||
#[command(SetNotePos = "note-pos")] fn edit_note_set_pos (&mut self, pos: usize) -> Perhaps<MidiEditCmd> {
|
||||
Ok(self.editor_mut().as_mut().map(|editor|{
|
||||
editor.set_note_pos((pos).min(127));
|
||||
editor.redraw();
|
||||
None
|
||||
}).flatten())
|
||||
}
|
||||
#[command(SetNoteLen = "note-len")] fn edit_note_set_len (&mut self, len: usize) -> Perhaps<MidiEditCmd> {
|
||||
Ok(self.editor_mut().as_mut().map(|editor|{
|
||||
editor.set_note_len(len);
|
||||
editor.redraw();
|
||||
None
|
||||
}).flatten())
|
||||
}
|
||||
#[command(SetNoteScroll = "note-scroll")] fn edit_note_set_scroll (&mut self, scroll: usize) -> Perhaps<MidiEditCmd> {
|
||||
Ok(self.editor_mut().as_mut().map(|editor|{
|
||||
editor.set_note_lo((scroll).min(127));
|
||||
editor.redraw();
|
||||
None
|
||||
}).flatten())
|
||||
}
|
||||
#[command(SetTimePos = "time-pos")] fn edit_time_set_pos (&mut self, pos: usize) -> Perhaps<MidiEditCmd> {
|
||||
Ok(self.editor_mut().as_mut().map(|editor|{
|
||||
editor.set_time_pos(pos);
|
||||
editor.redraw();
|
||||
None
|
||||
}).flatten())
|
||||
}
|
||||
#[command(SetTimeScroll = "time-scroll")] fn edit_time_set_scroll (&mut self, scroll: usize) -> Perhaps<MidiEditCmd> {
|
||||
Ok(self.editor_mut().as_mut().map(|editor|{
|
||||
editor.set_time_start(scroll);
|
||||
editor.redraw();
|
||||
None
|
||||
}).flatten())
|
||||
}
|
||||
#[command(SetTimeZoom = "time-zoom")] fn edit_time_set_zoom (&mut self, zoom: usize) -> Perhaps<MidiEditCmd> {
|
||||
Ok(self.editor_mut().as_mut().map(|editor|{
|
||||
editor.set_time_zoom(zoom);
|
||||
editor.redraw();
|
||||
None
|
||||
}).flatten())
|
||||
}
|
||||
#[command(SetTimeLock = "time-lock")] fn edit_time_set_lock (&mut self, lock: bool) -> Perhaps<MidiEditCmd> {
|
||||
Ok(self.editor_mut().as_mut().map(|editor|{
|
||||
editor.set_time_lock(lock);
|
||||
editor.redraw();
|
||||
None
|
||||
}).flatten())
|
||||
}
|
||||
// TODO: 1-9 seek markers that by default start every 8th of the clip
|
||||
}
|
||||
|
||||
impl<T: HasPool
|
||||
+ Namespace<bool>
|
||||
+ Namespace<usize>
|
||||
+ Namespace<Arc<str>>
|
||||
+ Namespace<PathBuf>
|
||||
+ Namespace<MidiClip>
|
||||
+ Namespace<ItemColor>
|
||||
+ Namespace<BrowseCmd>
|
||||
> PoolControl for T {}
|
||||
|
||||
#[tek_proc::commands(PoolCmd = "pool")] pub trait PoolControl: HasPool
|
||||
+ Namespace<bool>
|
||||
+ Namespace<usize>
|
||||
+ Namespace<Arc<str>>
|
||||
+ Namespace<PathBuf>
|
||||
+ Namespace<MidiClip>
|
||||
+ Namespace<ItemColor>
|
||||
+ Namespace<BrowseCmd>
|
||||
{
|
||||
/// Toggle visibility of pool
|
||||
#[command(Show = "show")] fn show (&mut self, visible: bool) -> Perhaps<PoolCmd> {
|
||||
self.pool_mut().visible = visible;
|
||||
Ok(Some(PoolCmd::Show { visible: !visible }))
|
||||
}
|
||||
/// Set selected clip
|
||||
#[command(Select = "select")] fn select (&mut self, index: usize) -> Perhaps<PoolCmd> {
|
||||
self.pool_mut().set_clip_index(index);
|
||||
Ok(None)
|
||||
}
|
||||
/// Delete a clip from the pool
|
||||
#[command(Delete = "delete")] fn delete (&mut self, index: usize) -> Perhaps<PoolCmd> {
|
||||
let clip = self.pool_mut().clips_mut().remove(index).try_read().unwrap().clone();
|
||||
Ok(Some(PoolCmd::Add { index, clip }))
|
||||
}
|
||||
/// Switch places of two clips in pool
|
||||
#[command(Swap = "swap")] fn swap (&mut self, index: usize, other: usize) -> Perhaps<PoolCmd> {
|
||||
self.pool_mut().clips_mut().swap(index, other);
|
||||
Ok(Some(PoolCmd::Swap { index, other }))
|
||||
}
|
||||
/// Add clip to pool
|
||||
#[command(Add = "add")] fn clip_add (&mut self, index: usize, clip: MidiClip) -> Perhaps<PoolCmd> {
|
||||
let mut index = index;
|
||||
let clip = Arc::new(RwLock::new(clip.clone()));
|
||||
let mut clips = self.pool_mut().clips_mut();
|
||||
if index >= clips.len() {
|
||||
index = clips.len();
|
||||
clips.push(clip)
|
||||
} else {
|
||||
clips.insert(index, clip);
|
||||
}
|
||||
Ok(Some(PoolCmd::Delete { index }))
|
||||
}
|
||||
/// Set name of clip
|
||||
#[command(SetName = "set-name")] fn clip_set_name (&mut self, index: usize, name: Arc<str>) -> Perhaps<PoolCmd> {
|
||||
let clip = &mut self.pool_mut().clips_mut()[index];
|
||||
let old_name = clip.try_read().unwrap().name.clone();
|
||||
clip.try_write().unwrap().name = name.clone();
|
||||
Ok(Some(PoolCmd::SetName { index, name: old_name }))
|
||||
}
|
||||
/// Set length of clip
|
||||
#[command(SetLength = "set-length")] fn clip_set_length (&mut self, index: usize, length: usize) -> Perhaps<PoolCmd> {
|
||||
let clip = &mut self.pool_mut().clips_mut()[index];
|
||||
let old_len = clip.try_read().unwrap().length;
|
||||
clip.try_write().unwrap().length = length;
|
||||
Ok(Some(PoolCmd::SetLength { index, length: old_len }))
|
||||
}
|
||||
/// Set color of clip
|
||||
#[command(SetColor = "set-color")] fn clip_set_color (&mut self, index: usize, color: ItemColor) -> Perhaps<PoolCmd> {
|
||||
let mut color = ItemTheme::from(color);
|
||||
std::mem::swap(&mut color, &mut self.pool().clips()[index].try_write().unwrap().color);
|
||||
Ok(Some(PoolCmd::SetColor { index, color: color.base }))
|
||||
}
|
||||
/// Enter crop mode
|
||||
#[command(CropBegin = "crop/begin")] fn crop_begin (&mut self) -> Perhaps<PoolCmd> {
|
||||
let index = self.pool().clip_index();
|
||||
let length = self.pool().clips()[index].try_read().unwrap().length;
|
||||
*self.pool_mut().mode_mut() = Some(PoolMode::Length(index, length, ClipLengthFocus::Bar));
|
||||
Ok(None)
|
||||
}
|
||||
/// Exit crop mode, discard
|
||||
#[command(CropCancel = "crop/cancel")] fn crop_cancel (&mut self) -> Perhaps<PoolCmd> {
|
||||
if let Some(PoolMode::Length(..)) = self.pool_mut().mode_mut().clone() {
|
||||
*self.pool_mut().mode_mut() = None;
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
#[command(CropSet = "crop/set")] fn crop_set (&mut self, _length: usize) -> Perhaps<PoolCmd> {
|
||||
if let Some(PoolMode::Length(clip, ref mut length, ref mut _focus))
|
||||
= self.pool_mut().mode_mut().clone()
|
||||
{
|
||||
let old_length;
|
||||
{
|
||||
let clip = self.pool().clips()[clip].clone();//.try_write().unwrap();
|
||||
old_length = Some(clip.try_read().unwrap().length);
|
||||
clip.try_write().unwrap().length = *length;
|
||||
}
|
||||
*self.pool_mut().mode_mut() = None;
|
||||
return Ok(old_length.map(|l|PoolCmd::CropSet { _length: l }))
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
#[command(CropNext = "crop/next")] fn crop_next (&mut self) -> Perhaps<PoolCmd> {
|
||||
if let Some(PoolMode::Length(
|
||||
_clip, ref mut _length, ref mut focus
|
||||
)) = self.pool_mut().mode_mut().clone() {
|
||||
focus.next()
|
||||
};
|
||||
Ok(None)
|
||||
}
|
||||
#[command(CropPrev = "crop/prev")] fn crop_prev (&mut self) -> Perhaps<PoolCmd> {
|
||||
if let Some(PoolMode::Length(
|
||||
_clip, ref mut _length, ref mut focus
|
||||
)) = self.pool_mut().mode_mut().clone() {
|
||||
focus.prev()
|
||||
};
|
||||
Ok(None)
|
||||
}
|
||||
#[command(CropInc = "crop/inc")] fn crop_inc (&mut self) -> Perhaps<PoolCmd> {
|
||||
if let Some(PoolMode::Length(
|
||||
_clip, ref mut length, ref mut focus
|
||||
)) = self.pool_mut().mode_mut().clone() {
|
||||
match focus {
|
||||
ClipLengthFocus::Bar => { *length += 4 * PPQ },
|
||||
ClipLengthFocus::Beat => { *length += PPQ },
|
||||
ClipLengthFocus::Tick => { *length += 1 },
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
#[command(CropDec = "crop/dec")] fn crop_dec (&mut self) -> Perhaps<PoolCmd> {
|
||||
if let Some(PoolMode::Length(
|
||||
_clip, ref mut length, ref mut focus
|
||||
)) = self.pool_mut().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)
|
||||
}
|
||||
/// Enter rename mode
|
||||
#[command(RenameBegin = "rename/begin")] fn rename_begin (&mut self) -> Perhaps<PoolCmd> {
|
||||
let index = self.pool().clip_index();
|
||||
let name = self.pool().clips()[index].try_read().unwrap().name.clone();
|
||||
*self.pool_mut().mode_mut() = Some(PoolMode::Rename(index, name));
|
||||
Ok(None)
|
||||
}
|
||||
/// Exit rename mode, discard
|
||||
#[command(RenameCancel = "rename/cancel")] fn rename_cancel (&mut self) -> Perhaps<PoolCmd> {
|
||||
if let Some(PoolMode::Rename(clip, ref mut old_name)) = self.pool_mut().mode_mut().clone() {
|
||||
self.pool().clips()[clip].try_write().unwrap().name = old_name.clone().into();
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
/// Exit rename mode, update name
|
||||
#[command(RenameConfirm = "rename/confirm")] fn rename_confirm (&mut self) -> Perhaps<PoolCmd> {
|
||||
Ok(if let Some(PoolMode::Rename(_clip, ref mut old_name)) = self.pool_mut().mode_mut().clone() {
|
||||
let old_name = old_name.clone();
|
||||
*self.pool_mut().mode_mut() = None;
|
||||
Some(PoolCmd::RenameSet { value: old_name })
|
||||
} else {
|
||||
None
|
||||
})
|
||||
}
|
||||
#[command(RenameSet = "rename/set")] fn rename_set (&mut self, value: Arc<str>) -> Perhaps<PoolCmd> {
|
||||
if let Some(PoolMode::Rename(clip, ref mut _old_name)) = self.pool_mut().mode_mut().clone() {
|
||||
self.pool().clips()[clip].try_write().unwrap().name = value.clone();
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
///// Import from file
|
||||
//#[command(Browse = "browse")]
|
||||
//fn browse (&mut self, command: BrowseCmd) -> Perhaps<PoolCmd> {
|
||||
//Ok(if let Some(browse) = self.pool_mut().browse.as_mut() {
|
||||
//command.act(browse)?.map(|command|PoolCmd::Browse{command})
|
||||
//} else {
|
||||
//None
|
||||
//})
|
||||
//}
|
||||
/// Import clip to pool from file
|
||||
#[command(Import = "import")]
|
||||
fn clip_import (&mut self, index: usize, path: PathBuf) -> Perhaps<PoolCmd>
|
||||
where Self: Sized
|
||||
{
|
||||
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(PoolCmd::Add { index, clip }.dispatch(self)?)
|
||||
}
|
||||
///// Export to file
|
||||
//#[command(Export = "export")]
|
||||
//fn export (&mut self, command: BrowseCmd) -> Perhaps<PoolCmd> {
|
||||
//Ok(if let Some(browse) = self.pool_mut().browse.as_mut() {
|
||||
//command.act(browse)?.map(|command|PoolCmd::Export{command})
|
||||
//} else {
|
||||
//None
|
||||
//})
|
||||
//}
|
||||
}
|
||||
|
||||
impl<T> ModeControl for T where T: HasModeStack
|
||||
+ Namespace<Vec<Arc<str>>>
|
||||
+ Namespace<Arc<str>>
|
||||
+ Namespace<usize>
|
||||
{}
|
||||
|
||||
#[tek_proc::commands(ModeCmd)] pub trait ModeControl: HasModeStack
|
||||
+ Namespace<Vec<Arc<str>>>
|
||||
+ Namespace<Arc<str>>
|
||||
+ Namespace<usize>
|
||||
{
|
||||
#[command(Reset = "reset")] fn mode_reset (&mut self, modes: Vec<Arc<str>>) -> Perhaps<ModeCmd> {
|
||||
*self.modes_mut() = modes.into();
|
||||
Ok(None)
|
||||
}
|
||||
#[command(Push = "push")] fn mode_push (&mut self, mode: Arc<str>) -> Perhaps<ModeCmd> {
|
||||
self.modes_mut().push(mode);
|
||||
Ok(Some(ModeCmd::Pop))
|
||||
}
|
||||
#[command(Pop = "pop")] fn mode_pop (&mut self) -> Perhaps<ModeCmd> {
|
||||
let mode = self.modes_mut().pop();
|
||||
Ok(mode.map(|mode|ModeCmd::Push { mode }))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: HasClips + Namespace<Option<bool>> + Namespace<Option<ItemTheme>> > MidiClipControl for T {}
|
||||
#[tek_proc::commands(MidiClipCmd)] pub trait MidiClipControl: HasClips
|
||||
+ Namespace<Option<bool>>
|
||||
+ Namespace<Option<ItemTheme>>
|
||||
{
|
||||
/// Create a clip at the current position if it does not already exist.
|
||||
#[command(Upsert = "upsert")] fn clip_upsert (&mut self) -> Perhaps<MidiClipCmd> {
|
||||
let clip = self.clip();
|
||||
//let old = self.scenes[scene].clips[track].clone();
|
||||
//self.scenes[scene].clips[track] = clip;
|
||||
//old
|
||||
todo!()
|
||||
}
|
||||
#[command(SetColor = "color")] fn clip_set_color (&mut self, color: Option<ItemTheme>) -> Perhaps<MidiClipCmd> {
|
||||
Ok(self.clip().map(|clip|MidiClipCmd::SetColor {
|
||||
color: Some(clip.write().set_color(color))
|
||||
}))
|
||||
}
|
||||
/// Toggle looping for the active clip
|
||||
#[command(SetLoop = "loop")] fn clip_toggle_loop (&mut self, looping: Option<bool>) -> Perhaps<MidiClipCmd> {
|
||||
Ok(self.clip().map(|clip|MidiClipCmd::SetLoop {
|
||||
looping: Some(clip.write().toggle_loop(looping))
|
||||
}))
|
||||
}
|
||||
}
|
||||
impl<T: Namespace<usize> + Namespace<PathBuf> + Namespace<Arc<str>>> BrowseControl for T {}
|
||||
#[tek_proc::commands(BrowseCmd = "browse")] pub trait BrowseControl:
|
||||
Namespace<usize> +
|
||||
Namespace<PathBuf> +
|
||||
Namespace<Arc<str>>
|
||||
{
|
||||
/// Toggle visibility of browser
|
||||
#[command(Show = "show")] fn show (&mut self) -> Perhaps<BrowseCmd> {
|
||||
todo!()
|
||||
}
|
||||
/// Set current directory
|
||||
#[command(Chdir = "chdir")] fn chdir (&mut self, _path: PathBuf) -> Perhaps<BrowseCmd> {
|
||||
todo!()
|
||||
}
|
||||
/// Set current filter
|
||||
#[command(SetSearch = "set-search")] fn set_search (&mut self, _filter: Arc<str>) -> Perhaps<BrowseCmd> {
|
||||
todo!()
|
||||
}
|
||||
/// Set selected item
|
||||
#[command(SetCursor = "set-cursor")] fn set_cursor (&mut self, _index: usize) -> Perhaps<BrowseCmd> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
#[tek_proc::commands(AxisCmd)]
|
||||
impl App {
|
||||
/// Increment a given data axis.
|
||||
#[command(Inc = "inc")]
|
||||
pub fn inc (&mut self, axis: ControlAxis) -> Perhaps<AxisCmd> {
|
||||
Ok(match (&self.dialog, axis) {
|
||||
(Dialog::None, _) => todo!(),
|
||||
(Dialog::Menu(_, _), ControlAxis::Y) => {
|
||||
AppCmd::Dialog { dialog: self.dialog.menu_next() }.dispatch(self)?;
|
||||
None
|
||||
},
|
||||
_ => todo!()
|
||||
})
|
||||
}
|
||||
/// Decrement a given data axis.
|
||||
#[command(Dec = "dec")]
|
||||
pub fn dec (&mut self, axis: ControlAxis) -> Perhaps<AxisCmd> {
|
||||
Ok(match (&self.dialog, axis) {
|
||||
(Dialog::None, _) => None,
|
||||
(Dialog::Menu(_, _), ControlAxis::Y) => {
|
||||
AppCmd::Dialog { dialog: self.dialog.menu_prev() }.dispatch(self)?;
|
||||
None
|
||||
},
|
||||
_ => todo!()
|
||||
})
|
||||
}
|
||||
}
|
||||
553
src/tek_view.rs
Normal file
553
src/tek_view.rs
Normal file
|
|
@ -0,0 +1,553 @@
|
|||
use crate::*;
|
||||
|
||||
pub fn draw_sample (
|
||||
to: &mut Tui, x: u16, y: u16, note: Option<&u7>, sample: &Sample, focus: bool
|
||||
) -> Usually<usize> {
|
||||
let style = if focus { Style::default().green() } else { Style::default() };
|
||||
if focus {
|
||||
to.blit(&"🬴", x+1, y, Some(style.bold()));
|
||||
}
|
||||
let label1 = format!("{:3} {:12}",
|
||||
note.map(|n|n.to_string()).unwrap_or(String::default()),
|
||||
sample.name);
|
||||
let label2 = format!("{:>6} {:>6} +0.0",
|
||||
sample.start,
|
||||
sample.end);
|
||||
to.blit(&label1, x+2, y, Some(style.bold()));
|
||||
to.blit(&label2, x+3+label1.len()as u16, y, Some(style));
|
||||
Ok(label1.len() + label2.len() + 4)
|
||||
}
|
||||
|
||||
pub fn draw_sample_list_item (sample: &Option<Arc<RwLock<Sample>>>) -> String {
|
||||
if let Some(sample) = sample {
|
||||
let sample = sample.try_read().unwrap();
|
||||
format!("{:8}", sample.name)
|
||||
//format!("{:8} {:3} {:6}-{:6}/{:6}",
|
||||
//sample.name,
|
||||
//sample.gain,
|
||||
//sample.start,
|
||||
//sample.end,
|
||||
//sample.channels[0].len()
|
||||
//)
|
||||
} else {
|
||||
String::from("........")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn draw_sample_viewer (sample: Option<&Arc<RwLock<Sample>>>) -> impl Draw<Tui> + use<'_> {
|
||||
let min_db = -64.0;
|
||||
draw(move|to: &mut Tui|{
|
||||
let xywh = to.area().into();
|
||||
let XYWH(x, y, width, height) = xywh;
|
||||
let area = Rect { x, y, width, height };
|
||||
if let Some(sample) = &sample {
|
||||
let sample = sample.try_read().unwrap();
|
||||
let start = sample.start as f64;
|
||||
let end = sample.end as f64;
|
||||
let length = end - start;
|
||||
let step = length / width as f64;
|
||||
let mut t = start;
|
||||
let mut lines = vec![];
|
||||
while t < end {
|
||||
let chunk = &sample.channels[0][t as usize..((t + step) as usize).min(sample.end)];
|
||||
let total: f32 = chunk.iter().map(|x|x.abs()).sum();
|
||||
let count = chunk.len() as f32;
|
||||
let meter = 10. * (total / count).log10();
|
||||
let x = t as f64;
|
||||
let y = meter as f64;
|
||||
lines.push(Line::new(x, min_db, x, y, Color::Green));
|
||||
t += step / 2.;
|
||||
}
|
||||
to.buffer().map(|buf|{
|
||||
Canvas::default()
|
||||
.x_bounds([sample.start as f64, sample.end as f64])
|
||||
.y_bounds([min_db, 0.])
|
||||
.paint(|ctx| {
|
||||
for line in lines.iter() {
|
||||
ctx.draw(line);
|
||||
}
|
||||
//FIXME: proportions
|
||||
//let text = "press record to finish sampling";
|
||||
//ctx.print(
|
||||
//(width - text.len() as u16) as f64 / 2.0,
|
||||
//height as f64 / 2.0,
|
||||
//text.red()
|
||||
//);
|
||||
})
|
||||
.render(area, buf);
|
||||
});
|
||||
} else {
|
||||
to.buffer().map(|buf|{
|
||||
Canvas::default()
|
||||
.x_bounds([0.0, width as f64])
|
||||
.y_bounds([0.0, height as f64])
|
||||
.paint(|_ctx| {
|
||||
//let text = "press record to begin sampling";
|
||||
//ctx.print(
|
||||
//(width - text.len() as u16) as f64 / 2.0,
|
||||
//height as f64 / 2.0,
|
||||
//text.red()
|
||||
//);
|
||||
})
|
||||
.render(area, buf);
|
||||
});
|
||||
}
|
||||
Ok(Some(xywh))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn draw_meters (meters: &[f32]) -> impl Draw<Tui> + use<'_> {
|
||||
bg(Black, iter_east_fixed(1, ||meters.iter(), |value, _index|{
|
||||
RmsMeter(*value).full_h()
|
||||
}).exact_w(2))
|
||||
}
|
||||
|
||||
#[cfg(feature = "lv2")]
|
||||
pub fn draw_lv2_header (state: &Lv2, to: &mut Tui, x: u16, y: u16, w: u16) {
|
||||
let style = Style::default().gray();
|
||||
let label1 = format!(" {}", state.name);
|
||||
to.blit(&label1, x + 1, y, Some(style.white().bold()));
|
||||
if let Some(ref path) = state.path {
|
||||
let label2 = format!("{}…", &path[..((w as usize - 10).min(path.len()))]);
|
||||
to.blit(&label2, x + 2 + label1.len() as u16, y, Some(style.not_dim()));
|
||||
}
|
||||
//Ok(Rect { x, y, width: w, height: 1 })
|
||||
}
|
||||
|
||||
pub fn draw_status_bar (to: &mut Tui, state: &App) -> Drawn<u16> {
|
||||
bar(Split::South, 1u16,
|
||||
dim(true, "Mode"),
|
||||
bold(true, state.modes.read().get(0))
|
||||
).push_x(1).draw(to)
|
||||
}
|
||||
|
||||
/// ```
|
||||
/// let x = "";
|
||||
/// let _ = tek::view_status(None, x.as_ref(), x.as_ref(), x.as_ref());
|
||||
/// let _ = tek::view_status(Some("".into()), x.as_ref(), x.as_ref(), x.as_ref());
|
||||
/// ```
|
||||
pub fn view_status <'a> (sel: Option<&str>, sr: &str, buf: &str, lat: &str) -> impl Draw<Tui> {
|
||||
let theme = ItemTheme::G[96];
|
||||
let sr = field_h(theme, "SR", sr);
|
||||
let buf = field_h(theme, "Buf", buf);
|
||||
let lat = field_h(theme, "Lat", lat);
|
||||
bg(Black, east!(above(
|
||||
sel.map(|sel|field_h(theme, "Selected", sel)).align_w().full_wh(),
|
||||
east!(sr, buf, lat).align_e().full_wh(),
|
||||
)))
|
||||
}
|
||||
|
||||
pub fn view_device <'a> (state: &'a App) -> impl Draw<Tui> {
|
||||
let selected = state.dialog.device_kind().unwrap();
|
||||
south(
|
||||
bold(true, "Add device"),
|
||||
iter_south(move||device_kinds().iter().enumerate().map(move|(i, _label)|{
|
||||
let b = if i == selected { Rgb(64,128,32) } else { Rgb(0,0,0) };
|
||||
let l = if i == selected { "[ " } else { " " };
|
||||
let r = if i == selected { " ]" } else { " " };
|
||||
bg(b, east(l, west(r, "FIXME device name"))).full_w()
|
||||
})))
|
||||
}
|
||||
|
||||
pub fn view_track_row_section <'a> (
|
||||
_theme: ItemTheme,
|
||||
button: impl Draw<Tui>,
|
||||
button_add: impl Draw<Tui>,
|
||||
content: impl Draw<Tui>,
|
||||
) -> impl Draw<Tui> {
|
||||
bar(
|
||||
Split::West,
|
||||
4u16,
|
||||
button_add.align_ne().full_wh(),
|
||||
bar(
|
||||
Split::East,
|
||||
20u16,
|
||||
button.full_wh().align_nw(),
|
||||
content.align_c().full_wh()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
pub fn view_track_header <'a> (theme: ItemTheme, content: impl Draw<Tui>)
|
||||
-> impl Draw<Tui>
|
||||
{
|
||||
bg(theme.darker.term, content.align_e().full_w()).exact_w(12)
|
||||
}
|
||||
|
||||
pub fn view_scene_name <'a> (
|
||||
select: &Selection,
|
||||
editor: Option<&'a MidiEditor>,
|
||||
index: usize,
|
||||
scene: &Scene,
|
||||
editing: bool
|
||||
) -> impl Draw<Tui> {
|
||||
let h = if select.scene() == Some(index) && let Some(_editor) = editor {
|
||||
7
|
||||
} else {
|
||||
Scene::DEFAULT_HEIGHT as u16
|
||||
};
|
||||
let a = east!("·s", index, " ", fg(g(255), bold(true, &scene.name))).align_w().full_w();
|
||||
let b = when(select.scene() == Some(index) && editing, south(
|
||||
editor.as_ref().map(|e|e.clip_status()),
|
||||
editor.as_ref().map(|e|e.edit_status())).align_nw().full_wh());
|
||||
let c = if select.scene() == Some(index) {
|
||||
scene.color.light.term
|
||||
} else {
|
||||
scene.color.base.term
|
||||
};
|
||||
bg(c, south(a, b).align_nw().exact_h(h))
|
||||
}
|
||||
|
||||
pub fn view_scene_name_theme (scene: &Scene, track_index: usize) -> (Arc<str>, ItemTheme) {
|
||||
if let Some(Some(clip)) = &scene.clips.get(track_index) {
|
||||
let clip = clip.try_read().unwrap();
|
||||
(format!(" ⏹ {}", &clip.name).into(), clip.color)
|
||||
} else {
|
||||
(" ⏹ -- ".into(), ItemTheme::G[32])
|
||||
}
|
||||
}
|
||||
|
||||
pub fn view_scene_bg (
|
||||
theme: ItemTheme, select: &Selection, track_index: usize, scene_index: usize
|
||||
) -> (Color, Color) {
|
||||
let mut outline = theme.base.term;
|
||||
(if select.track() == Some(track_index) && select.scene() == Some(scene_index) {
|
||||
outline = theme.lighter.term;
|
||||
theme.light.term
|
||||
} else if select.track() == Some(track_index) || select.scene() == Some(scene_index) {
|
||||
outline = theme.darkest.term;
|
||||
theme.base.term
|
||||
} else {
|
||||
theme.dark.term
|
||||
}, outline)
|
||||
}
|
||||
|
||||
pub fn view_scene_w (
|
||||
track: &Track, select: &Selection, track_index: usize, editor: Option<&MidiEditor>
|
||||
) -> u16 {
|
||||
if select.track() == Some(track_index) && let Some(editor) = editor {
|
||||
(editor.size.w() as usize).max(24).max(track.width) as u16
|
||||
} else {
|
||||
track.width as u16
|
||||
}
|
||||
}
|
||||
|
||||
pub fn view_scene_y (
|
||||
select: &Selection, scene_index: usize, editor: Option<&MidiEditor>
|
||||
) -> u16 {
|
||||
if select.scene() == Some(scene_index) && let Some(editor) = editor {
|
||||
editor.size.h().max(12)
|
||||
} else {
|
||||
Scene::DEFAULT_HEIGHT as u16
|
||||
}
|
||||
}
|
||||
|
||||
pub fn view_scene_sel (select: &Selection, track_index: usize, scene_index: usize, editing: bool) -> bool {
|
||||
editing && select.track() == Some(track_index) && select.scene() == Some(scene_index)
|
||||
}
|
||||
|
||||
pub fn view_templates <'a> (state: &'a App) -> impl Draw<Tui> + use<'a> {
|
||||
let height = (state.config.modes.len() * 2) as u16;
|
||||
draw(move |to: &mut Tui|{
|
||||
let mut index = 0;
|
||||
state.config.modes.for_each(&mut |id: &str, profile: &Mode| {
|
||||
let b = if index == 0 { Rgb(70,70,70) } else { Rgb(50,50,50) };
|
||||
let name = profile.name.get(0).map(|x|x.as_ref()).unwrap_or("<no name>");
|
||||
let info = profile.info.get(0).map(|x|x.as_ref()).unwrap_or("<no info>");
|
||||
let fg1 = Rgb(224, 192, 128);
|
||||
let fg2 = Rgb(224, 128, 32);
|
||||
let field_name = fg(fg1, name).align_w().full_w();
|
||||
let field_id = fg(fg2, id).align_e().full_w();
|
||||
let field_info = info.align_w().full_w();
|
||||
let _ = bg(b, south(above(field_name, field_id), field_info))
|
||||
.full_w().exact_h(2).push_y((2 * index) as u16).draw(to);
|
||||
index += 1;
|
||||
});
|
||||
Ok(Some(to.area().into()))
|
||||
}).min_w(30).exact_h(height)
|
||||
}
|
||||
|
||||
pub fn view_browse_title (state: &App) -> impl Draw<Tui> {
|
||||
field_v(ItemTheme::default(), match state.dialog.browser_target().unwrap() {
|
||||
BrowseTarget::SaveProject => "Save project:",
|
||||
BrowseTarget::LoadProject => "Load project:",
|
||||
BrowseTarget::ImportSample(_) => "Import sample:",
|
||||
BrowseTarget::ExportSample(_) => "Export sample:",
|
||||
BrowseTarget::ImportClip(_) => "Import clip:",
|
||||
BrowseTarget::ExportClip(_) => "Export clip:",
|
||||
}, fg(g(96), x_repeat("🭻")).exact_h(1)).align_w().full_w()
|
||||
}
|
||||
|
||||
/// ```
|
||||
/// let _ = tek::view_meter("", 0.0);
|
||||
/// let _ = tek::view_meters(&[0.0, 0.0]);
|
||||
/// ```
|
||||
pub fn view_meter <'a> (label: &'a str, value: f32)
|
||||
-> impl Draw<Tui>
|
||||
{
|
||||
let f = field_h(ItemTheme::G[128], label, format!("{:>+9.3}", value));
|
||||
let w = if value >= 0.0 { 13 }
|
||||
else if value >= -1.0 { 12 }
|
||||
else if value >= -2.0 { 11 }
|
||||
else if value >= -3.0 { 10 }
|
||||
else if value >= -4.0 { 9 }
|
||||
else if value >= -6.0 { 8 }
|
||||
else if value >= -9.0 { 7 }
|
||||
else if value >= -12.0 { 6 }
|
||||
else if value >= -15.0 { 5 }
|
||||
else if value >= -20.0 { 4 }
|
||||
else if value >= -25.0 { 3 }
|
||||
else if value >= -30.0 { 2 }
|
||||
else if value >= -40.0 { 1 }
|
||||
else { 0 };
|
||||
let c = if value >= 0.0 { Red }
|
||||
else if value >= -3.0 { Yellow }
|
||||
else { Green };
|
||||
south!(f, bg(c, ()).exact_wh(w, 1))
|
||||
}
|
||||
|
||||
pub fn view_meters (values: &[f32;2]) -> impl Draw<Tui> + use<'_> {
|
||||
let left = format!("L/{:>+9.3}", values[0]);
|
||||
let right = format!("R/{:>+9.3}", values[1]);
|
||||
south(left, right)
|
||||
}
|
||||
|
||||
pub fn view_midi_ins_status (theme: ItemTheme, track: Option<&Track>) -> impl Draw<Tui> {
|
||||
track.map(move|track|view_ports_status(theme, "MIDI ins: ", &track.sequencer.midi_ins))
|
||||
}
|
||||
|
||||
pub fn view_midi_outs_status (theme: ItemTheme, track: Option<&Track>) -> impl Draw<Tui> {
|
||||
track.map(move|track|view_ports_status(theme, "MIDI outs: ", &track.sequencer.midi_outs))
|
||||
}
|
||||
|
||||
pub fn view_audio_ins_status (theme: ItemTheme, track: Option<&Track>) -> impl Draw<Tui> {
|
||||
track.map(move|track|view_ports_status(theme, "Audio ins: ", &track.audio_ins()))
|
||||
}
|
||||
|
||||
pub fn view_audio_outs_status (theme: ItemTheme, track: Option<&Track>) -> impl Draw<Tui> {
|
||||
track.map(move|track|view_ports_status(theme, "Audio outs:", &track.audio_outs()))
|
||||
}
|
||||
|
||||
pub fn view_ports_status <'a, T: JackPort> (theme: ItemTheme, title: &'a str, ports: &'a [T])
|
||||
-> impl Draw<Tui> + use<'a, T>
|
||||
{
|
||||
let ins = ports.len() as u16;
|
||||
let frame = Outer(true, Style::default().fg(g(96)));
|
||||
border(true, frame, field_v(theme, title, iter_south({
|
||||
move||ports.iter().enumerate().map(|(index, port)|{
|
||||
east!(" ", index, " ", port.port_name()).align_w().full_h()
|
||||
})
|
||||
})).exact_wh(20, 1 + ins)).exact_wh(20, 1 + ins)
|
||||
}
|
||||
|
||||
pub fn view_io_ports <'a, T: PortsSizes<'a>> (
|
||||
fg: Color, bg: Color, items: impl Fn()->T + Send + Sync + 'a
|
||||
) -> impl Draw<Tui> + 'a {
|
||||
type Item<'a> = (usize, &'a Arc<str>, &'a [Connect], usize, usize);
|
||||
iter(items,
|
||||
move|(_index, name, connections, y, y2): Item<'a>, _| south(
|
||||
bold(true, fg_bg(fg, bg, east(" ", name).align_w())).full_h(),
|
||||
iter(||connections.iter(), move|connect: &'a Connect, index|{
|
||||
bold(false, fg_bg(fg, bg, &connect.info)).exact_h(1).align_w().push_y(index as u16)
|
||||
})
|
||||
).exact_h((y2 - y) as u16).push_y(y as u16))
|
||||
}
|
||||
|
||||
pub fn view_sample_info (sample: Option<&Arc<RwLock<Sample>>>) -> impl Draw<Tui> + use<'_> {
|
||||
when(sample.is_some(), draw(move|to: &mut Tui|{
|
||||
let sample = sample.unwrap().try_read().unwrap();
|
||||
let theme = sample.color;
|
||||
east!(
|
||||
field_h(theme, "Name", format!("{:<10}", sample.name.clone())),
|
||||
field_h(theme, "Length", format!("{:<8}", sample.channels[0].len())),
|
||||
field_h(theme, "Start", format!("{:<8}", sample.start)),
|
||||
field_h(theme, "End", format!("{:<8}", sample.end)),
|
||||
field_h(theme, "Trans", "0"),
|
||||
field_h(theme, "Gain", format!("{}", sample.gain)),
|
||||
).draw(to)
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn view_sample_info_v (sample: Option<&Arc<RwLock<Sample>>>) -> impl Draw<Tui> + use<'_> {
|
||||
let a = draw(move|to: &mut Tui|{
|
||||
let sample = sample.unwrap().try_read().unwrap();
|
||||
let theme = sample.color;
|
||||
south!(
|
||||
field_h(theme, "Name ", format!("{:<10}", sample.name.clone())) .align_w().full_w(),
|
||||
field_h(theme, "Length", format!("{:<8}", sample.channels[0].len())).align_w().full_w(),
|
||||
field_h(theme, "Start ", format!("{:<8}", sample.start)) .align_w().full_w(),
|
||||
field_h(theme, "End ", format!("{:<8}", sample.end)) .align_w().full_w(),
|
||||
field_h(theme, "Trans ", "0") .align_w().full_w(),
|
||||
field_h(theme, "Gain ", format!("{}", sample.gain)) .align_w().full_w(),
|
||||
).exact_w(20).draw(to)
|
||||
});
|
||||
|
||||
let b = draw(|to: &mut Tui|fg(Red, south!(
|
||||
bold(true, "× No sample."),
|
||||
"[r] record",
|
||||
"[Shift-F9] import",
|
||||
)).draw(to));
|
||||
|
||||
either(sample.is_some(), a, b)
|
||||
}
|
||||
|
||||
pub fn view_sample_status (sample: Option<&Arc<RwLock<Sample>>>) -> impl Draw<Tui> {
|
||||
bold(true, fg(g(224), sample
|
||||
.map(|sample|{
|
||||
let sample = sample.try_read().unwrap();
|
||||
format!("Sample {}-{}", sample.start, sample.end)
|
||||
})
|
||||
.unwrap_or_else(||"No sample".to_string())))
|
||||
}
|
||||
|
||||
/// ```
|
||||
/// let _ = tek::button_2("", "", true);
|
||||
/// let _ = tek::button_2("", "", false);
|
||||
/// ```
|
||||
pub fn button_2 <'a> (key: impl Draw<Tui>, label: impl Draw<Tui>, hide: bool)
|
||||
-> impl Draw<Tui>
|
||||
{
|
||||
let c1 = tui_orange();
|
||||
let c2 = tui_g(0);
|
||||
let c3 = tui_g(96);
|
||||
let c4 = tui_g(255);
|
||||
bold(true, fg_bg(c1, c2,
|
||||
bar(Split::East, 1u16, fg(c2, "▐"),
|
||||
bar(Split::East, 1u16, key,
|
||||
bar(Split::East, 1u16, fg(c3, "▐"),
|
||||
when(!hide, fg_bg(c4, c3, label)))))))
|
||||
}
|
||||
|
||||
/// ```
|
||||
/// let _ = tek::button_3("", "", "", true);
|
||||
/// let _ = tek::button_3("", "", "", false);
|
||||
/// ```
|
||||
pub fn button_3 <'a> (
|
||||
key: impl Draw<Tui>,
|
||||
label: impl Draw<Tui>,
|
||||
value: impl Draw<Tui>,
|
||||
editing: bool,
|
||||
) -> impl Draw<Tui> {
|
||||
let c1 = tui_orange();
|
||||
let c2 = tui_g(0);
|
||||
let c3 = tui_g(96);
|
||||
let c4 = tui_g(255);
|
||||
let c5 = tui_g(128);
|
||||
bold(true, bar(Split::East, 3u16,
|
||||
fg_bg(c1, c2,
|
||||
bar(Split::East, 1u16, fg(c2, "▐"),
|
||||
bar(Split::East, 1u16, key, fg(if editing { c5 } else { c3 }, "▐")))),
|
||||
east(
|
||||
when(!editing, east(fg_bg(c4, c3, label), fg_bg(c5, c3, "▐"),)),
|
||||
east(fg_bg(tui_g(224), c5, value), fg_bg(c5, Reset, "▌"), ))))
|
||||
}
|
||||
|
||||
pub fn field_h <'a, T: Screen> (
|
||||
_theme: ItemTheme, _head: impl Draw<T>, _body: impl Draw<T>
|
||||
) -> impl Draw<T> {
|
||||
}
|
||||
|
||||
pub fn field_v <'a, T: Screen> (
|
||||
_theme: ItemTheme, _head: impl Draw<T>, _body: impl Draw<T>
|
||||
) -> impl Draw<T> {
|
||||
}
|
||||
|
||||
pub fn view_sessions <'a> () -> impl Draw<Tui> {
|
||||
let h = 6;
|
||||
let w = Some(30);
|
||||
let f = Rgb(224, 192, 128);
|
||||
draw(move |to: &mut Tui|{
|
||||
for (index, name) in ["session1", "session2", "session3"].iter().enumerate() {
|
||||
let b = if index == 0 { Rgb(50,50,50) } else { Rgb(40,40,40) };
|
||||
let y = (2 * index) as u16;
|
||||
let h = 2;
|
||||
bg(b, fg(f, *name).align_w()).full_w().exact_h(h).push_y(y).draw(to)?;
|
||||
}
|
||||
Ok(Some(to.area().into()))
|
||||
}).min_w(w).exact_h(h)
|
||||
}
|
||||
|
||||
/// ```
|
||||
/// let bg = tengri::ratatui::style::Color::Red;
|
||||
/// let fg = tengri::ratatui::style::Color::Green;
|
||||
/// let _ = tek::view_wrap(bg, fg, "and then blue, too!");
|
||||
/// ```
|
||||
pub fn view_wrap <'a> (bg: Color, fg: Color, content: impl Draw<Tui>)
|
||||
-> impl Draw<Tui>
|
||||
{
|
||||
let left = fg_bg(bg, Reset, y_repeat("▐").exact_w(1));
|
||||
let right = fg_bg(bg, Reset, y_repeat("▌").exact_w(1));
|
||||
east(left, west(right, fg_bg(fg, bg, content)))
|
||||
}
|
||||
|
||||
#[macro_export] macro_rules! rewrite {
|
||||
($buf:ident, $($rest:tt)*) => {
|
||||
|$buf,_,_|{
|
||||
$buf.clear();
|
||||
write!($buf, $($rest)*)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub struct Memo<T, U> {
|
||||
value: T,
|
||||
view: U,
|
||||
}
|
||||
|
||||
impl<T, U> Memo<T, U> {
|
||||
pub fn new (value: T, view: U) -> Self {
|
||||
Self { value, view }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: PartialEq + Debug, U: Debug> Debug for Memo<T, U> {
|
||||
fn fmt (&self, _: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: PartialEq, U> Memo<T, U> {
|
||||
pub fn update <R> (&mut self, newval: T, render: impl Fn(&mut U, &T, &T)->R) -> Option<R> {
|
||||
if newval != self.value {
|
||||
let result = render(&mut self.view, &newval, &self.value);
|
||||
self.value = newval;
|
||||
return Some(result);
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Gettable<T> {
|
||||
/// Returns current value
|
||||
fn get (&self) -> T;
|
||||
}
|
||||
|
||||
pub trait Mutable<T>: Gettable<T> {
|
||||
/// Sets new value, returns old
|
||||
fn set (&mut self, value: T) -> T;
|
||||
}
|
||||
|
||||
pub trait InteriorMutable<T>: Gettable<T> {
|
||||
/// Sets new value, returns old
|
||||
fn set (&self, value: T) -> T;
|
||||
}
|
||||
|
||||
#[cfg(test)] mod test_view {
|
||||
use super::*;
|
||||
use proptest::prelude::*;
|
||||
proptest! {
|
||||
|
||||
#[test] fn proptest_view_meter (
|
||||
label in "\\PC*", value in f32::MIN..f32::MAX
|
||||
) {
|
||||
let _ = view_meter(&label, value);
|
||||
}
|
||||
|
||||
#[test] fn proptest_view_meters (
|
||||
value1 in f32::MIN..f32::MAX,
|
||||
value2 in f32::MIN..f32::MAX
|
||||
) {
|
||||
let _ = view_meters(&[value1, value2]);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue