more reorg; newtype Binds and Views

This commit is contained in:
i do not exist 2026-09-19 10:11:55 +03:00
parent 3e3276bed0
commit f10fa309e5
27 changed files with 1017 additions and 892 deletions

View file

@ -1,6 +1,5 @@
#export RUSTFLAGS := "--cfg procmacro2_semver_exempt -Zmacro-backtrace -Clink-arg=-fuse-ld=mold"
export RUST_BACKTRACE := "1"
export RUSTFLAGS := "-Zmacro-backtrace -Clink-arg=-fuse-ld=mold -Clink-arg=-Wl,--no-rosegment -Cforce-frame-pointers=yes"
export RUSTFLAGS := "-Clink-arg=-fuse-ld=mold -Clink-arg=-Wl,--no-rosegment -Cforce-frame-pointers=yes"
[default]
list:

View file

@ -24,28 +24,32 @@ use crate::*;
/// let _ = app.project.h_scenes();
/// ```
#[derive(Default, Debug)]
#[namespace(MidiClip)]
#[namespace(ItemColor)]
#[namespace(ItemTheme)]
#[namespace(isize)]
#[namespace(u8)]
#[namespace(bool get_bool)]
#[namespace(u16 get_u16)]
#[namespace(usize get_usize)]
#[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,
@ -83,7 +87,7 @@ impl App {
/// ```
/// 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 mut conf = tek::Config::load(tek::Config::default(), "(mode hello)").unwrap();
/// let tek = tek::App::new(None, proj, conf.into(), "hello");
/// ```
pub fn new (
@ -128,6 +132,10 @@ fn get_usize <'a> (state: &App, src: impl Language) -> Perhaps<usize> {
}))
}
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,
@ -145,7 +153,7 @@ fn get_bool <'a> (state: &App, src: impl Language) -> Perhaps<bool> {
":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(format!("not bool: {word}").into())
_ => return err!("not bool: {word}")
})).transpose()
}
@ -158,7 +166,7 @@ fn get_selection <'a> (state: &App, src: impl Language) -> Perhaps<Selection> {
":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(format!("not selection: {word}").into())
_ => return err!("not selection: {word}")
})).transpose()
}
@ -179,64 +187,64 @@ fn get_color <'a> (_: &App, src: impl Language) -> Perhaps<Color> {
.ok_or(LanguageError::domain("not blue"))?;
Ok(Some(Color::Rgb(r, g, b)))
},
(Some(_), _) => return Err(format!("not a color expression: {expr}").into()),
(None, _) => return Err(format!("not a color expression: {expr}").into()),
(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(format!("not a color: {sym}").into())
_ => return err!("not a color: {sym}")
})
} else {
return Err(format!("not a color: {:?}", src.src()?).into())
return err!("not a color: {:?}", src.src()?)
}
}
#[allow(unused)]
fn get_opt_u7 <'a> (state: &App, src: impl Language) -> Perhaps<Option<u7>> {
src.word()?.map(|word|Ok(match word {
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(format!("unknown midi note: {word}").into())
})).transpose()
_ => return err!("unknown midi note: {word}")
}))
}
fn get_opt_usize <'a> (state: &App, src: impl Language) -> Perhaps<Option<usize>> {
src.word()?.map(|word|Ok(match word {
eval_word(src, |word|Ok(match word {
":selected/scene" => state.selection().scene(),
":selected/track" => state.selection().track(),
_ => return Err(format!("unknown opt<usize>: {word}").into())
})).transpose()
}
#[allow(unused)]
fn get_clip <'a> (state: &App, src: impl Language) -> Perhaps<Option<Arc<RwLock<MidiClip>>>> {
src.word()?.map(|word|Ok(match word {
":selected/clip" if let Selection::TrackClip { track, scene } = state.selection() =>
state.scenes()[*scene].clips[*track].clone(),
_ => return Err(format!("not a clip: {word}").into())
})).transpose()
_ => return err!("unknown opt<usize>: {word}")
}))
}
fn get_axis <'a> (_: &App, src: impl Language) -> Perhaps<ControlAxis> {
Ok(src.word()?.map(|word|Ok(match word {
eval_word(src, |word|Ok(match word {
":x" => ControlAxis::X,
":y" => ControlAxis::Y,
_ => return Err(format!("unknown axis {word}"))
})).transpose()?)
_ => return err!("unknown axis {word}")
}))
}
fn get_opt_u16 <'a> (_: &App, _src: impl Language) -> Perhaps<Option<u16>> {
Ok(None)
}
fn get_opt_itemtheme <'a> (_: &App, _: impl Language) -> Perhaps<Option<ItemTheme>> {
todo!()
fn get_opt_u32 <'a> (_: &App, _src: impl Language) -> Perhaps<Option<u32>> {
Ok(None)
}
fn get_opt_vec_opt_arc_rwlock_midiclip <'a> (_: &App, _: impl Language) -> Perhaps<Option<Vec<Option<Arc<RwLock<MidiClip>>>>>> {
fn get_opt_itemtheme <'a> (_: &App, _: impl Language) -> Perhaps<Option<ItemTheme>> {
todo!()
}
@ -269,9 +277,9 @@ 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_opt!(MidiEditor: |self: App|self.project.as_ref_opt());
impl_as_mut_opt!(MidiEditor: |self: App|self.project.as_mut_opt());
impl_has_clips!( |self: App|self.pool.clips);
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);
@ -298,7 +306,7 @@ impl App {
if let Some(mode) = mode {
let binds = state.config.binds.clone();
for id in mode.keys.iter() {
if let Some(event_map) = binds.try_read().unwrap().get(id.as_ref())
if let Some(event_map) = binds.find(id)
&& let Some(bindings) = event_map.query(input) {
return Some(bindings.into())
}
@ -379,7 +387,7 @@ impl App {
/// Swap currently active modal dialog.
///
/// ```
/// let _ = tek::App::default().set_dialog(tek::Dialog::welcome());
/// let _ = tek::App::default().command_dialog(tek::Dialog::welcome());
/// ```
#[command(Dialog = "dialog")]
pub fn command_dialog (&mut self, dialog: Dialog) -> Perhaps<AppCommand> {
@ -413,6 +421,11 @@ impl App {
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))
@ -480,6 +493,7 @@ impl App {
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 });

View file

@ -1,38 +0,0 @@
use crate::*;
/// Collection of input bindings.
pub type Binds = Arc<RwLock<BTreeMap<Arc<str>, Bind<TuiEvent, Arc<str>>>>>;
pub fn binds_add <'a> (binds: &Binds, expr: impl Language) -> UsuallyRef<'a, ()> {
//println!("\n\rbinds_add: {expr:?}");
let name = expr.head()?.ok_or("bind: missing name")?;
let body = expr.tail()?.unwrap_or("");
binds.try_write().unwrap().insert(name.into(), {
let mut map = Bind::new();
body.each((), |_, item: &str|if matches!(item.expr().head(), Ok(Some("see"))) {
// TODO
Ok(())
} else if let Ok(Some(_word)) = item.expr().head().word() {
let expr = item.expr()?;
let head = expr.head()?;
if let Some(event) = TuiKey::from_dsl(&head)?.to_crossterm() {
map.add(TuiEvent(event), Binding {
commands: [item.expr()?.tail()?.unwrap_or_default().into()].into(),
condition: None,
description: None,
source: None
});
Ok(())
} else if Some(":char") == item.expr()?.head()? {
// TODO
return Ok(())
} else {
return Err(format!("binds_add: invalid key: {:?}", item.expr()?.head()?).into())
}
} else {
return Err(format!("binds_add: unexpected: {item:?}").into())
})?;
map
});
Ok(())
}

View file

@ -215,10 +215,12 @@ pub fn print_status (project: &Project) {
pub fn print_config (config: &Config) {
use ::ansi_term::Color::*;
println!("{:?}", config.dirs);
for (k, v) in config.views.try_read().unwrap().iter() {
config.views.for_each(|k, v|{
println!("{} {} {}", Green.paint("VIEW"), Green.bold().paint(format!("{k:<16}")), v.source);
}
for (k, v) in config.binds.try_read().unwrap().iter() {
});
config.binds.for_each(|k, v|{
println!("{} {}", Green.paint("BIND"), Green.bold().paint(format!("{k:<16}")));
for (k, v) in v.0.iter() {
print!("{} ", &Yellow.paint(match &k.0 {
@ -238,7 +240,8 @@ pub fn print_config (config: &Config) {
//println!(" {:?}", v.source);
}
}
}
});
config.modes.for_each(|k, v|{
println!();
for v in v.name.iter() { print!("{}", Green.bold().paint(format!("{v} "))); }

View file

@ -1,136 +0,0 @@
use crate::*;
impl Project {
/// Toggle looping for the active clip
pub fn toggle_loop (&mut self) {
if let Some(clip) = self.selected_clip() {
clip.try_write().unwrap().toggle_loop()
}
}
/// Put a clip in a slot
pub fn clip_put (
&mut self, track: usize, scene: usize, clip: Option<Arc<RwLock<MidiClip>>>
) -> Option<Arc<RwLock<MidiClip>>> {
let old = self.scenes[scene].clips[track].clone();
self.scenes[scene].clips[track] = clip;
old
}
/// Change the color of a clip, returning the previous one
pub fn clip_set_color (
&self, track: usize, scene: usize, color: ItemTheme
) -> Option<ItemTheme> {
self.scenes[scene].clips[track].as_ref().map(|clip|{
let mut clip = clip.try_write().unwrap();
let old = clip.color.clone();
clip.color = color.clone();
panic!("{color:?} {old:?}");
//old
})
}
}
pub trait HasClipsSize {
fn clips_size (&self) -> &Sizer;
}
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 }
}
impl<T: TracksView + ScenesView + Send + Sync> ClipsView for T {}
pub trait ClipsView: TracksView + ScenesView {
/// Draw clips per scene
fn view_scenes_clips (&self) -> impl Draw<Tui> {
let select = self.selection();
let editor = self.editor();
let editing = self.is_editing();
with_clips_size(true, self.clips_size(), iter_east(move||self.tracks_with_sizes()
.map(move|(track_index, track, _, _)|iter_south(move||self.scenes_with_sizes()
.map(move|(scene_index, scene, _, _)|{
let (name, theme): (Arc<str>, ItemTheme) = view_scene_name_theme(scene, track_index);
let f = theme.lightest.term;
let (b, o) = view_scene_bg(theme, select, track_index, scene_index);
let is_selected = view_scene_sel(select, track_index, scene_index, editing);
below(
Outer(true, Style::default().fg(o)).full_wh(),
below(
below(
fg_bg(o, b, "".full_wh()),
fg_bg(f, b, bold(true, name)).align_nw().full_wh(),
),
when(is_selected, editor).full_wh()
).full_wh()
).exact_wh(
view_scene_w(track, select, track_index, editor),
view_scene_y(select, scene_index, editor),
)
})
)
.exact_w(track.width as u16))
)).align_c()
}
}
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])
}
}
fn with_clips_size (show: bool, size: &Sizer, content: impl Draw<Tui>) -> impl Draw<Tui> {
above(
when(show, fg(Green, east!(size.w() as usize, "x", size.h() as usize)).align_se()),
size.of(content.align_c().full_wh())
)
}
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)
}
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
}
}
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
}
}
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)
}

View file

@ -5,7 +5,7 @@ use notify_debouncer_full::notify::{RecursiveMode, PollWatcher, Config as Notify
/// Configuration: mode, view, and bind definitions.
///
/// ```
/// use ::{std::sync::Arc, tek::{Config, Modes::add, Mode}};
/// use ::{std::sync::Arc, tek::{Config, Modes, Mode}};
/// let config: Config = Default::default();
/// Modes::add(&config.modes, stringify!(
/// (mode :menu (name Menu)
@ -58,24 +58,18 @@ impl Config {
pub fn load_item <C: AsRef<Config>, L: Language> (config: C, src: L) -> Usually<C> {
if let Some(expr) = src.expr()? {
Config::load_kind(config, 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("view") => Views::add(&config.as_ref().views, expr.tail()),
_ => return Err(format!("Config::load: expected view/keys/mode expr, got: {src:?}").into())
}?;
Ok(config)
} else {
Err(format!("Config::add_one: tried to add empty item").into())
}
}
pub fn load_kind <C: AsRef<Config>, L: Language> (config: C, src: L) -> Usually<C> {
//if let Some(tail) = src.tail()? {
match src.head()? {
Some("mode") => Modes::add(&config.as_ref().modes, src.tail()),
Some("keys") => binds_add(&config.as_ref().binds, src.tail()),
Some("view") => views_add(&config.as_ref().views, src.tail()),
_ => return Err(format!("Config::load: expected view/keys/mode expr, got: {src:?}").into())
}?;
//}
Ok(config)
}
/// Watch a config's file for changes.
pub fn watch (
config: Arc<Config>, poll: Option<Duration>
@ -158,14 +152,14 @@ impl Config {
}
/// Make this configuration empty.
fn clear (&self) {
pub fn clear (&self) {
*self.modes.0.try_write().unwrap() = Default::default();
*self.views.try_write().unwrap() = Default::default();
*self.binds.try_write().unwrap() = Default::default();
self.views.clear();
self.binds.clear();
}
pub fn get_view (&self, name: impl AsRef<str>) -> Option<Arc<View<Tui, App>>> {
self.views.try_read().map(|views|views.get(name.as_ref()).cloned()).flatten()
self.views.get(name)
}
/// Default configuration directory.
@ -181,3 +175,12 @@ impl Config {
const DEFAULTS: &'static str = include_str!("tek.edn");
}
pub fn eval_word <T> (src: impl Language, eval: impl Fn(&str)->Usually<T>) -> Perhaps<T> {
src.word()?.map(eval).transpose()
}
//pub fn eval_expr <T> (src: impl Language, eval: impl Fn(&str)->Usually<T>) -> Perhaps<T> {
//Ok(match expr.head()? {
//})
//}

View file

@ -1,5 +1,63 @@
use crate::*;
/// Collection of input bindings.
#[derive(Default, Clone, Debug)]
pub struct Binds(pub Arc<RwLock<BTreeMap<
Arc<str>,
Arc<Bind<TuiEvent, Arc<str>>>
>>>);
impl Binds {
pub fn add <'a> (binds: &Binds, 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::new();
body.each((), |_, item: &str|if matches!(item.expr().head(), Ok(Some("see"))) {
// TODO
Ok(())
} else if let Ok(Some(_word)) = item.expr().head().word() {
let expr = item.expr()?;
let head = expr.head()?;
if let Some(event) = TuiKey::from_dsl(&head)?.to_crossterm() {
map.add(TuiEvent(event), Binding {
commands: [item.expr()?.tail()?.unwrap_or_default().into()].into(),
condition: None,
description: None,
source: None
});
Ok(())
} else if Some(":char") == item.expr()?.head()? {
// TODO
return Ok(())
} else {
return err!("Binds::add: invalid key: {:?}", item.expr()?.head()?)
}
} else {
return err!("Binds::add: unexpected: {item:?}")
})?;
map.into()
});
Ok(())
}
pub fn clear (&self) {
*self.0.write() = Default::default();
}
pub fn find (&self, id: impl AsRef<str>) -> Option<Arc<Bind<TuiEvent, Arc<str>>>> {
self.0.read().get(id.as_ref()).cloned()
}
pub fn for_each <T> (
&self, mut cb: impl FnMut(&Arc<str>, &Bind<TuiEvent, Arc<str>>)->T
) {
for (k, v) in self.0.read().iter() {
cb(k, v);
}
}
}
/// An map of input events (e.g. [TuiEvent]) to [Binding]s.
///
/// ```

162
src/config_mode.rs Normal file
View file

@ -0,0 +1,162 @@
use crate::*;
/// Collection of UI modes.
///
/// ```
/// let mut modes = tek::Modes::default();
/// assert_eq!(modes.len(), 0);
/// let _ = modes.add(&":foo", &"(mode (name Foo) (info Bar))");
/// assert_eq!(modes.len(), 1);
/// ```
#[derive(Default, Debug, Clone)]
pub struct Modes(pub Arc<RwLock<BTreeMap<
Arc<str>,
Arc<Mode>
>>>);
impl Modes {
/// Register a mode.
pub fn add <'a> (&self, expr: impl Language) -> UsuallyRef<'a, ()> {
let name = expr.head()?.ok_or("mode: missing name")?;
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));
Ok(())
}
/// Get a mode by name.
pub fn get (&self, name: impl AsRef<str>) -> Option<Arc<Mode>> {
self.0.try_read().unwrap().get(name.as_ref()).cloned()
}
/// 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() {
let _ = ator(k.as_ref(), v.as_ref());
}
}
/// Count modes.
pub fn len (&self) -> usize {
self.0.try_read().unwrap().len()
}
}
/// Group of view and keys definitions.
///
/// ```
/// let mode = tek::Mode::default();
/// ```
#[derive(Default, Debug)]
pub struct Mode {
pub path: PathBuf,
pub name: Vec<Arc<str>>,
pub info: Vec<Arc<str>>,
pub view: Vec<Arc<View<Tui, App>>>,
pub keys: Vec<Arc<str>>,
pub modes: Modes,
}
impl Mode {
/// Add a definition to the mode.
///
/// Supported definitions:
///
/// - (name ...) -> name
/// - (info ...) -> description
/// - (keys ...) -> key bindings
/// - (mode ...) -> submode
/// - ... -> view
///
/// ```
/// let mut mode: tek::Mode = tek::Mode::add(tek::Mode::default(), "(name hello)").unwrap();
/// ```
pub fn add <T: AsMut<Mode>> (mut mode: T, dsl: impl Language) -> Usually<T> {
Ok(if let Ok(Some(expr)) = dsl.expr() && let Ok(Some(head)) = expr.head() {
//println!("Mode::add: {head} {:?}", expr.tail());
let tail = expr.tail()?.map(|x|x.trim()).unwrap_or("");
match head {
"mode" => {
let name = tail.head()?.ok_or("submode: missing name")?;
let body = tail.tail()?.ok_or("submode: missing body")?;
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));
mode
},
"keys" => {
tail.each(mode, |mut mode: T, expr: &str|{
mode.as_mut().keys.push(expr.trim().into());
Ok(mode)
})?
},
"name" => { mode.as_mut().name.push(tail.into()); mode },
"info" => { mode.as_mut().info.push(tail.into()); mode },
"view" => { mode.as_mut().view.push(View::new(tail)?.into()); mode },
_ => { mode.as_mut().view.push(View::new(tail)?.into()); mode },
}
} else if let Ok(Some(word)) = dsl.word() {
mode.as_mut().view.push(View::new(word)?.into());
mode
} else {
return Err(format!("Mode::add: unexpected: {dsl:?}").into());
})
}
}
impl AsMut<Mode> for Mode {
fn as_mut (&mut self) -> &mut Self {
self
}
}
pub trait HasModeStack {
fn modes (&self) -> RwLockReadGuard<'_, Vec<Arc<str>>>;
fn modes_mut (&mut self) -> RwLockWriteGuard<'_, Vec<Arc<str>>>;
}
impl HasModeStack for App {
fn modes (&self) -> RwLockReadGuard<'_, Vec<Arc<str>>> {
self.modes.try_read().unwrap()
}
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 }))
}
}

View file

@ -1,7 +1,36 @@
use crate::*;
/// Collection of custom view definitions.
pub type Views = Arc<RwLock<BTreeMap<Arc<str>, Arc<View<Tui, App>>>>>;
#[derive(Default, Clone, Debug)]
pub struct Views(pub Arc<RwLock<BTreeMap<
Arc<str>,
Arc<View<Tui, App>>
>>>);
impl Views {
/// Load custom view definition.
pub fn add <'a> (views: &Views, expr: impl Language) -> UsuallyRef<'a, ()> {
views.0.write().insert(
expr.head()?.ok_or("view: missing name")?.into(),
View::new(expr.tail()?.ok_or("view: missing body")?)?.into()
);
Ok(())
}
pub fn clear (&self) {
*self.0.try_write().unwrap() = Default::default();
}
pub fn get (&self, name: impl AsRef<str>) -> Option<Arc<View<Tui, App>>> {
self.0.try_read().map(|views|views.get(name.as_ref()).cloned()).flatten()
}
pub fn for_each <T> (&self, cb: impl Fn(&Arc<str>, &Arc<View<Tui, App>>)->T) -> () {
for (k, v) in self.0.read().iter() {
cb(k, v);
}
}
}
/// Custom view definition is a boxed closure emitting a [Draw]able from state `S`.
pub struct View<S: Screen, T> {
@ -37,7 +66,7 @@ impl View<Tui, App> {
} else if let Ok(Some(word)) = source.word() {
Self::compile_word(word.into())?
} else {
return Err(format!("not word/expr:\n{source:?}").into())
return err!("not word/expr:\n{source:?}")
};
Ok(Arc::new(Box::new(move|state, screen|layer(state, screen))))
}
@ -94,7 +123,7 @@ impl View<Tui, App> {
Some("w") => Split::West,
Some("a") => Split::Above,
Some("b") => Split::Below,
_ => return Err(format!("invalid split: {split:?}").into())
_ => return err!("invalid split: {split:?}")
};
let a = Self::compile(arg!(expr, head, 1, "content A"))?;
let b = Self::compile(arg!(expr, head, 2, "content B"))?;
@ -113,7 +142,7 @@ impl View<Tui, App> {
Some("w") => Split::West,
Some("a") => Split::Above,
Some("b") => Split::Below,
_ => return Err(format!("invalid split: {split:?}").into())
_ => return err!("invalid split: {split:?}")
};
let size = Arc::from(arg!(expr, head, 1, "size"));
let size = move|state: &App|state.namespace(&size)?.ok_or_else(||Box::<dyn Error>::from("bar: no size"));
@ -140,7 +169,7 @@ impl View<Tui, App> {
Some("c") => Azimuth::C,
Some("x") => Azimuth::X,
Some("y") => Azimuth::Y,
_ => return Err(format!("invalid azimuth: {azimuth:?}").into())
_ => return err!("invalid azimuth: {azimuth:?}")
};
let thunk = Self::compile(arg!(expr, head, 1, "content"))?;
Self::boxed(move|state, screen|{
@ -239,12 +268,12 @@ impl View<Tui, App> {
})
},
_ => return Err(format!("compile_expr: unexpected: {expr:?}").into())
_ => return err!("compile_expr: unexpected: {expr:?}")
}
} else {
return Err(format!("compile_expr: invalid expression: {expr:?}").into())
return err!("compile_expr: invalid expression: {expr:?}")
}))
}
@ -310,12 +339,3 @@ impl View<Tui, App> {
}))
}
}
/// Load custom view definition.
pub fn views_add <'a> (views: &Views, expr: impl Language) -> UsuallyRef<'a, ()> {
views.try_write().unwrap().insert(
expr.head()?.ok_or("view: missing name")?.into(),
View::new(expr.tail()?.ok_or("view: missing body")?)?.into()
);
Ok(())
}

View file

@ -1,12 +1,17 @@
use crate::*;
pub fn get_dialog <'a> (_: &App, src: impl Language) -> Perhaps<Dialog> {
src.word()?.map(|word|Ok(match word {
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,
@ -14,22 +19,25 @@ pub fn get_dialog <'a> (_: &App, src: impl Language) -> Perhaps<Dialog> {
":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()
)
})).transpose()
_ => return Err(format!("not a dialog: {}", src.src()?.unwrap_or("<???>")).into())
}))
}
/// Various possible dialog modes.

View file

@ -1,5 +1,37 @@
use crate::*;
pub fn get_opt_arc_rwlock_midiclip <'a> (state: &App, src: impl Language)
-> Perhaps<Option<Arc<RwLock<MidiClip>>>>
{
eval_word(src, |word|Ok(match word {
":selected" if let Selection::TrackClip {
track, scene
} = state.selection() => {
state.scenes()[*scene].clips[*track].clone()
},
//":selected-or-create" if let Selection::TrackClip {
//track, scene
//} = state.selection() => Some(if let Some(Some(clip)) = state.scenes()[*scene].clips.get(*track) {
//clip.clone()
//} else {
//let clip: Arc<RwLock<MidiClip>> = Arc::new(RwLock::new(Default::default()));
//state.scenes()[*scene].clips[*track] = Some(clip.clone());
//clip
//}),
_ => return err!("not a clip: {word}")
}))
}
pub fn get_opt_vec_opt_arc_rwlock_midiclip <'a> (_: &App, _: impl Language)
-> Perhaps<Option<Vec<Option<Arc<RwLock<MidiClip>>>>>>
{
todo!()
}
/// A MIDI sequence.
///
/// ```
@ -93,6 +125,8 @@ impl MidiClip {
}
pub type MidiData = Vec<Vec<MidiMessage>>;
impl PartialEq for MidiClip { fn eq (&self, other: &Self) -> bool { self.uuid == other.uuid } }
impl Eq for MidiClip {}
#[macro_export] macro_rules! has_clip {
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
@ -122,8 +156,12 @@ pub trait MidiClipController: HasMidiClip
+ Namespace<Option<bool>>
+ Namespace<Option<ItemTheme>>
{
#[command(Upsert = "upsert")]
fn clip_upsert (&mut self) -> Perhaps<MidiClipCommand> {
todo!()
}
#[command(SetColor = "clip/color")]
#[command(SetColor = "color")]
fn clip_set_color (&mut self, _color: Option<ItemTheme>) -> Perhaps<MidiClipCommand> {
//(SetColor [t: usize, s: usize, c: ItemTheme]
//clip.clip_set_color(t, s, c).map(|o|Self::SetColor(t, s, o)))));
@ -131,7 +169,7 @@ pub trait MidiClipController: HasMidiClip
todo!()
}
#[command(SetLoop = "clip/loop")]
#[command(SetLoop = "loop")]
fn clip_toggle_loop (&mut self, _looping: Option<bool>) -> Perhaps<MidiClipCommand> {
//(SetLoop [t: usize, s: usize, l: bool] cmd_todo!("\n\rtodo: {self:?}"))
//("loop" [a: usize, b: usize, c: bool] Some(Self::SetLoop(a.unwrap(), b.unwrap(), c.unwrap())))
@ -139,11 +177,3 @@ pub trait MidiClipController: HasMidiClip
}
}
impl PartialEq for MidiClip {
fn eq (&self, other: &Self) -> bool {
self.uuid == other.uuid
}
}
impl Eq for MidiClip {}

View file

@ -26,3 +26,90 @@ pub trait HasClips {
}
}
}
impl Project {
/// Toggle looping for the active clip
pub fn toggle_loop (&mut self) {
if let Some(clip) = self.selected_clip() {
clip.try_write().unwrap().toggle_loop()
}
}
/// Put a clip in a slot
pub fn clip_put (
&mut self, track: usize, scene: usize, clip: Option<Arc<RwLock<MidiClip>>>
) -> Option<Arc<RwLock<MidiClip>>> {
let old = self.scenes[scene].clips[track].clone();
self.scenes[scene].clips[track] = clip;
old
}
/// Change the color of a clip, returning the previous one
pub fn clip_set_color (
&self, track: usize, scene: usize, color: ItemTheme
) -> Option<ItemTheme> {
self.scenes[scene].clips[track].as_ref().map(|clip|{
let mut clip = clip.try_write().unwrap();
let old = clip.color.clone();
clip.color = color.clone();
panic!("{color:?} {old:?}");
//old
})
}
}
pub trait HasClipsSize {
fn clips_size (&self) -> &Sizer;
}
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 }
}
impl<T: TracksView + ScenesView + Send + Sync> ClipsView for T {}
pub trait ClipsView: TracksView + ScenesView {
/// Draw clips per scene
fn view_scenes_clips (&self) -> impl Draw<Tui> {
let select = self.selection();
let editor = self.editor();
let editing = self.is_editing();
with_clips_size(true, self.clips_size(), iter_east(move||self.tracks_with_sizes()
.map(move|(track_index, track, _, _)|iter_south(move||self.scenes_with_sizes()
.map(move|(scene_index, scene, _, _)|{
let (name, theme): (Arc<str>, ItemTheme) = view_scene_name_theme(scene, track_index);
let f = theme.lightest.term;
let (b, o) = view_scene_bg(theme, select, track_index, scene_index);
let is_selected = view_scene_sel(select, track_index, scene_index, editing);
below(
Outer(true, Style::default().fg(o)).full_wh(),
below(
below(
fg_bg(o, b, "".full_wh()),
fg_bg(f, b, bold(true, name)).align_nw().full_wh(),
),
when(is_selected, editor).full_wh()
).full_wh()
).exact_wh(
view_scene_w(track, select, track_index, editor.as_ref()),
view_scene_y(select, scene_index, editor.as_ref()),
)
})
)
.exact_w(track.width as u16))
)).align_c()
}
}
fn with_clips_size (show: bool, size: &Sizer, content: impl Draw<Tui>) -> impl Draw<Tui> {
above(
when(show, fg(Green, east!(size.w() as usize, "x", size.h() as usize)).align_se()),
size.of(content.align_c().full_wh())
)
}

View file

@ -1,70 +1,27 @@
#![allow(unused)]
use crate::*;
impl App {
/// Is a MIDI editor currently focused?
///
/// ```
/// tek::App::default().editor_focused();
/// ```
pub fn editor_focused (&self) -> bool {
false
pub trait HasEditor: AsRef<Option<MidiEditor>> + AsMut<Option<MidiEditor>> {
fn editor (&self) -> &Option<MidiEditor> {
self.as_ref()
}
/// Toggle MIDI editor.
///
/// ```
/// tek::App::default().toggle_editor(None);
/// ```
pub fn toggle_editor (&mut self, value: Option<bool>) {
//FIXME: self.editing.store(value.unwrap_or_else(||!self.is_editing()), Relaxed);
let value = value.unwrap_or_else(||!self.editor().is_some());
if value {
// Create new clip in pool when entering empty cell
if let Selection::TrackClip { track, scene } = *self.selection()
&& let Some(scene) = self.project.scenes.get_mut(scene)
&& let Some(slot) = scene.clips.get_mut(track)
&& slot.is_none()
&& let Some(track) = self.project.tracks.get_mut(track)
{
let (_index, clip) = self.pool.add_new_clip();
// autocolor: new clip colors from scene and track color
let color = track.color.base.mix(scene.color.base, 0.5);
clip.try_write().unwrap().color = ItemColor::random_near(color, 0.2).into();
if let Some(editor) = &mut self.project.editor {
editor.set_clip(Some(&clip));
fn editor_mut (&mut self) -> &mut Option<MidiEditor> {
self.as_mut()
}
*slot = Some(clip.clone());
//Some(clip)
} else {
//None
}
} else if let Selection::TrackClip { track, scene } = *self.selection()
&& let Some(scene) = self.project.scenes.get_mut(scene)
&& let Some(slot) = scene.clips.get_mut(track)
&& let Some(clip) = slot.as_mut()
{
// Remove clip from arrangement when exiting empty clip editor
let mut swapped = None;
if clip.try_read().unwrap().count_midi_messages() == 0 {
std::mem::swap(&mut swapped, slot);
}
if let Some(clip) = swapped {
self.pool.delete_clip(&clip.try_read().unwrap());
fn is_editing (&self) -> bool {
self.editor().is_some()
}
fn editor_w (&self) -> usize {
self.editor().as_ref().map(|e|e.size.w()).unwrap_or(0) as usize
}
fn editor_h (&self) -> usize {
self.editor().as_ref().map(|e|e.size.h()).unwrap_or(0) as usize
}
}
pub trait HasEditor: AsRefOpt<MidiEditor> + AsMutOpt<MidiEditor> {
fn editor (&self) -> Option<&MidiEditor> { self.as_ref_opt() }
fn editor_mut (&mut self) -> Option<&mut MidiEditor> { self.as_mut_opt() }
fn is_editing (&self) -> bool { self.editor().is_some() }
fn editor_w (&self) -> usize { self.editor().map(|e|e.size.w()).unwrap_or(0) as usize }
fn editor_h (&self) -> usize { self.editor().map(|e|e.size.h()).unwrap_or(0) as usize }
}
impl<T: AsRefOpt<MidiEditor>+AsMutOpt<MidiEditor>> HasEditor for T {}
impl<T> HasEditor for T where T:
AsRef<Option<MidiEditor>> +
AsMut<Option<MidiEditor>> {}
impl<T: HasEditor
+ Namespace<u32>
@ -75,7 +32,7 @@ impl<T: HasEditor
+ Namespace<Option<Arc<RwLock<MidiClip>>>>
> MidiEditController for T {}
#[tek_proc::commands(MidiEditCommand = "edit")]
#[tek_proc::commands(MidiEditCommand)]
pub trait MidiEditController: HasEditor
+ Namespace<u32>
+ Namespace<f64>
@ -84,9 +41,19 @@ pub trait MidiEditController: HasEditor
+ 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 show (&mut self, clip: Option<Arc<RwLock<MidiClip>>>) -> Perhaps<MidiEditCommand> {
Ok(self.editor_mut().map(|editor|{
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
@ -94,16 +61,16 @@ pub trait MidiEditController: HasEditor
}
#[command(DeleteNote = "delete")]
fn note_delete (&mut self) -> Perhaps<MidiEditCommand> {
Ok(self.editor_mut().map(|editor|{
fn edit_note_delete (&mut self) -> Perhaps<MidiEditCommand> {
Ok(self.editor_mut().as_mut().map(|editor|{
editor.redraw();
todo!()
}).flatten())
}
#[command(AppendNote = "append")]
fn note_append (&mut self, advance: bool) -> Perhaps<MidiEditCommand> {
Ok(self.editor_mut().map(|editor|{
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
@ -111,8 +78,8 @@ pub trait MidiEditController: HasEditor
}
#[command(SetNotePos = "note-pos")]
fn note_set_pos (&mut self, pos: usize) -> Perhaps<MidiEditCommand> {
Ok(self.editor_mut().map(|editor|{
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
@ -120,8 +87,8 @@ pub trait MidiEditController: HasEditor
}
#[command(SetNoteLen = "note-len")]
fn note_set_len (&mut self, len: usize) -> Perhaps<MidiEditCommand> {
Ok(self.editor_mut().map(|editor|{
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
@ -129,8 +96,8 @@ pub trait MidiEditController: HasEditor
}
#[command(SetNoteScroll = "note-scroll")]
fn note_set_scroll (&mut self, scroll: usize) -> Perhaps<MidiEditCommand> {
Ok(self.editor_mut().map(|editor|{
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
@ -138,8 +105,8 @@ pub trait MidiEditController: HasEditor
}
#[command(SetTimePos = "time-pos")]
fn time_set_pos (&mut self, pos: usize) -> Perhaps<MidiEditCommand> {
Ok(self.editor_mut().map(|editor|{
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
@ -147,8 +114,8 @@ pub trait MidiEditController: HasEditor
}
#[command(SetTimeScroll = "time-scroll")]
fn time_set_scroll (&mut self, scroll: usize) -> Perhaps<MidiEditCommand> {
Ok(self.editor_mut().map(|editor|{
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
@ -156,8 +123,8 @@ pub trait MidiEditController: HasEditor
}
#[command(SetTimeZoom = "time-zoom")]
fn time_set_zoom (&mut self, zoom: usize) -> Perhaps<MidiEditCommand> {
Ok(self.editor_mut().map(|editor|{
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
@ -165,13 +132,14 @@ pub trait MidiEditController: HasEditor
}
#[command(SetTimeLock = "time-lock")]
fn time_set_lock (&mut self, lock: bool) -> Perhaps<MidiEditCommand> {
Ok(self.editor_mut().map(|editor|{
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
}
@ -432,3 +400,58 @@ impl Draw<Tui> for MidiEditor {
self.size.of(&self.mode).draw(to)
}
}
impl App {
/// Is a MIDI editor currently focused?
///
/// ```
/// tek::App::default().editor_focused();
/// ```
pub fn editor_focused (&self) -> bool {
false
}
/// Toggle MIDI editor.
///
/// ```
/// tek::App::default().toggle_editor(None);
/// ```
pub fn toggle_editor (&mut self, value: Option<bool>) {
//FIXME: self.editing.store(value.unwrap_or_else(||!self.is_editing()), Relaxed);
let value = value.unwrap_or_else(||!self.editor().is_some());
if value {
// Create new clip in pool when entering empty cell
if let Selection::TrackClip { track, scene } = *self.selection()
&& let Some(scene) = self.project.scenes.get_mut(scene)
&& let Some(slot) = scene.clips.get_mut(track)
&& slot.is_none()
&& let Some(track) = self.project.tracks.get_mut(track)
{
let (_index, clip) = self.pool.add_new_clip();
// autocolor: new clip colors from scene and track color
let color = track.color.base.mix(scene.color.base, 0.5);
clip.try_write().unwrap().color = ItemColor::random_near(color, 0.2).into();
if let Some(editor) = &mut self.project.editor {
editor.set_clip(Some(&clip));
}
*slot = Some(clip.clone());
//Some(clip)
} else {
//None
}
} else if let Selection::TrackClip { track, scene } = *self.selection()
&& let Some(scene) = self.project.scenes.get_mut(scene)
&& let Some(slot) = scene.clips.get_mut(track)
&& let Some(clip) = slot.as_mut()
{
// Remove clip from arrangement when exiting empty clip editor
let mut swapped = None;
if clip.try_read().unwrap().count_midi_messages() == 0 {
std::mem::swap(&mut swapped, slot);
}
if let Some(clip) = swapped {
self.pool.delete_clip(&clip.try_read().unwrap());
}
}
}
}

View file

@ -1,72 +0,0 @@
use crate::*;
/// Group of view and keys definitions.
///
/// ```
/// let mode = tek::Mode::default();
/// ```
#[derive(Default, Debug)]
pub struct Mode {
pub path: PathBuf,
pub name: Vec<Arc<str>>,
pub info: Vec<Arc<str>>,
pub view: Vec<Arc<View<Tui, App>>>,
pub keys: Vec<Arc<str>>,
pub modes: Modes,
}
impl Mode {
/// Add a definition to the mode.
///
/// Supported definitions:
///
/// - (name ...) -> name
/// - (info ...) -> description
/// - (keys ...) -> key bindings
/// - (mode ...) -> submode
/// - ... -> view
///
/// ```
/// let mut mode: tek::Mode = tek::Mode::add(tek::Mode::default(), "(name hello)").unwrap();
/// ```
pub fn add <T: AsMut<Mode>> (mut mode: T, dsl: impl Language) -> Usually<T> {
Ok(if let Ok(Some(expr)) = dsl.expr() && let Ok(Some(head)) = expr.head() {
//println!("Mode::add: {head} {:?}", expr.tail());
let tail = expr.tail()?.map(|x|x.trim()).unwrap_or("");
match head {
"mode" => {
let name = tail.head()?.ok_or("submode: missing name")?;
let body = tail.tail()?.ok_or("submode: missing body")?;
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));
mode
},
"keys" => {
tail.each(mode, |mut mode: T, expr: &str|{
mode.as_mut().keys.push(expr.trim().into());
Ok(mode)
})?
},
"name" => { mode.as_mut().name.push(tail.into()); mode },
"info" => { mode.as_mut().info.push(tail.into()); mode },
"view" => { mode.as_mut().view.push(View::new(tail)?.into()); mode },
_ => { mode.as_mut().view.push(View::new(tail)?.into()); mode },
}
} else if let Ok(Some(word)) = dsl.word() {
mode.as_mut().view.push(View::new(word)?.into());
mode
} else {
return Err(format!("Mode::add: unexpected: {dsl:?}").into());
})
}
}
impl AsMut<Mode> for Mode {
fn as_mut (&mut self) -> &mut Self {
self
}
}

View file

@ -1,46 +0,0 @@
use crate::*;
pub trait HasModeStack {
fn modes (&self) -> RwLockReadGuard<Vec<Arc<str>>>;
fn modes_mut (&mut self) -> RwLockWriteGuard<Vec<Arc<str>>>;
}
impl HasModeStack for App {
fn modes (&self) -> RwLockReadGuard<Vec<Arc<str>>> {
self.modes.try_read().unwrap()
}
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 }))
}
}

View file

@ -1,45 +0,0 @@
use crate::*;
/// Collection of UI modes.
///
/// ```
/// let mut modes = tek::Modes::default();
/// assert_eq!(modes.len(), 0);
/// let _ = modes.add(&":foo", &"(mode (name Foo) (info Bar))");
/// assert_eq!(modes.len(), 1);
/// ```
#[derive(Default, Debug, Clone)]
pub struct Modes(
pub Arc<RwLock<BTreeMap<Arc<str>, Arc<Mode>>>>
);
impl Modes {
/// Register a mode.
pub fn add <'a> (modes: &Modes, expr: impl Language) -> UsuallyRef<'a, ()> {
let name = expr.head()?.ok_or("mode: missing name")?;
let body = expr.tail()?.ok_or("mode: missing body")?;
let mode = Mode::default();
let mode = body.each(mode, |c, s|Mode::add(c, s))?;
modes.0.try_write().unwrap().insert(name.into(), Arc::new(mode));
Ok(())
}
/// Get a mode by name.
pub fn get (&self, name: impl AsRef<str>) -> Option<Arc<Mode>> {
self.0.try_read().unwrap().get(name.as_ref()).cloned()
}
/// 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() {
let _ = ator(k.as_ref(), v.as_ref());
}
}
/// Count modes.
pub fn len (&self) -> usize {
self.0.try_read().unwrap().len()
}
}

View file

@ -142,8 +142,11 @@ impl_has!(Jack<'static>: |self: Project| self.jack);
impl_has!(Sizer: |self: Project| self.size);
impl_has!(Clock: |self: Project| self.clock);
impl_has!(Selection: |self: Project| self.selection);
impl_as_ref_opt!(MidiEditor: |self: Project| self.editor.as_ref());
impl_as_mut_opt!(MidiEditor: |self: Project| self.editor.as_mut());
impl_has!(Vec<Scene>: |self: Project| self.scenes);
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);
pub fn swap_value <T: Clone + PartialEq, U> (
target: &mut T, value: &T, returned: impl Fn(T)->U

144
src/sample.rs Normal file
View file

@ -0,0 +1,144 @@
use crate::*;
/// A sound cut.
///
/// ```
/// let sample = tek::Sample::default();
/// let sample = tek::Sample::new("test", 0, 0, vec![]);
/// ```
#[derive(Default, Debug)] pub struct Sample {
pub name: Arc<str>,
pub start: usize,
pub end: usize,
pub channels: Vec<Vec<f32>>,
pub rate: Option<usize>,
pub gain: f32,
pub color: ItemTheme,
}
impl Sample {
pub fn new (name: impl AsRef<str>, start: usize, end: usize, channels: Vec<Vec<f32>>) -> Self {
Self {
name: name.as_ref().into(),
start,
end,
channels,
rate: None,
gain: 1.0,
color: ItemTheme::random(),
}
}
pub fn play (sample: &Arc<RwLock<Self>>, after: usize, velocity: &u7) -> Voice {
Voice {
sample: sample.clone(),
after,
position: sample.try_read().unwrap().start,
velocity: velocity.as_int() as f32 / 127.0,
}
}
pub fn handle_cc (&mut self, controller: u7, value: u7) {
let percentage = value.as_int() as f64 / 127.;
match controller.as_int() {
20 => {
self.start = (percentage * self.end as f64) as usize;
},
21 => {
let length = self.channels[0].len();
self.end = length.min(
self.start + (percentage * (length as f64 - self.start as f64)) as usize
);
},
22 => { /*attack*/ },
23 => { /*decay*/ },
24 => {
self.gain = percentage as f32 * 2.0;
},
26 => { /* pan */ }
25 => { /* pitch */ }
_ => {}
}
}
/// Read WAV from file
pub fn read_data (src: &str) -> Usually<(usize, Vec<Vec<f32>>)> {
let mut channels: Vec<wavers::Samples<f32>> = vec![];
for channel in wavers::Wav::from_path(src)?.channels() {
channels.push(channel);
}
let mut end = 0;
let mut data: Vec<Vec<f32>> = vec![];
for samples in channels.iter() {
let channel = Vec::from(samples.as_ref());
end = end.max(channel.len());
data.push(channel);
}
Ok((end, data))
}
pub fn from_file (path: &PathBuf) -> Usually<Self> {
let name = path.file_name().unwrap().to_string_lossy().into();
let mut sample = Self { name, ..Default::default() };
// Use file extension if present
let mut hint = Hint::new();
if let Some(ext) = path.extension() {
hint.with_extension(&ext.to_string_lossy());
}
let probed = symphonia::default::get_probe().format(
&hint,
MediaSourceStream::new(
Box::new(File::open(path)?),
Default::default(),
),
&Default::default(),
&Default::default()
)?;
let mut format = probed.format;
let params = &format.tracks().iter()
.find(|t| t.codec_params.codec != CODEC_TYPE_NULL)
.expect("no tracks found")
.codec_params;
let mut decoder = get_codecs().make(params, &Default::default())?;
loop {
match format.next_packet() {
Ok(packet) => sample.decode_packet(&mut decoder, packet)?,
Err(symphonia::core::errors::Error::IoError(_)) => break decoder.last_decoded(),
Err(err) => return Err(err.into()),
};
};
sample.end = sample.channels.iter().fold(0, |l, c|l + c.len());
Ok(sample)
}
fn decode_packet (
&mut self, decoder: &mut Box<dyn Decoder>, packet: Packet
) -> Usually<()> {
// Decode a packet
let decoded = decoder
.decode(&packet)
.map_err(|e|Box::<dyn std::error::Error>::from(e))?;
// Determine sample rate
let spec = *decoded.spec();
if let Some(rate) = self.rate {
if rate != spec.rate as usize {
panic!("sample rate changed");
}
} else {
self.rate = Some(spec.rate as usize);
}
// Determine channel count
while self.channels.len() < spec.channels.count() {
self.channels.push(vec![]);
}
// Load sample
let mut samples = SampleBuffer::new(
decoded.frames() as u64,
spec
);
if samples.capacity() > 0 {
samples.copy_interleaved_ref(decoded);
for frame in samples.samples().chunks(spec.channels.count()) {
for (chan, frame) in frame.iter().enumerate() {
self.channels[chan].push(*frame)
}
}
}
Ok(())
}
}

128
src/sample_add.rs Normal file
View file

@ -0,0 +1,128 @@
use crate::*;
#[derive(Default, Debug)] pub struct SampleAdd {
pub exited: bool,
pub dir: PathBuf,
pub subdirs: Vec<OsString>,
pub files: Vec<OsString>,
pub cursor: usize,
pub offset: usize,
pub sample: Arc<RwLock<Sample>>,
pub voices: Arc<RwLock<Vec<Voice>>>,
pub _search: Option<String>,
}
impl_draw!(|self: SampleAdd, to: Tui|{ todo!() });
#[allow(unused)]
impl SampleAdd {
fn exited (&self) -> bool {
self.exited
}
fn exit (&mut self) {
self.exited = true
}
pub fn new (
sample: &Arc<RwLock<Sample>>,
voices: &Arc<RwLock<Vec<Voice>>>
) -> Usually<Self> {
let dir = std::env::current_dir()?;
let (subdirs, files) = scan(&dir)?;
Ok(Self {
exited: false,
dir,
subdirs,
files,
cursor: 0,
offset: 0,
sample: sample.clone(),
voices: voices.clone(),
_search: None
})
}
fn rescan (&mut self) -> Usually<()> {
scan(&self.dir).map(|(subdirs, files)|{
self.subdirs = subdirs;
self.files = files;
})
}
fn prev (&mut self) {
self.cursor = self.cursor.saturating_sub(1);
}
fn next (&mut self) {
self.cursor = self.cursor + 1;
}
fn try_preview (&mut self) -> Usually<()> {
if let Some(path) = self.cursor_file() {
if let Ok(sample) = Sample::from_file(&path) {
*self.sample.try_write().unwrap() = sample;
self.voices.try_write().unwrap().push(
Sample::play(&self.sample, 0, &u7::from(100u8))
);
}
//load_sample(&path)?;
//let src = std::fs::File::open(&path)?;
//let mss = MediaSourceStream::new(Box::new(src), Default::default());
//let mut hint = Hint::new();
//if let Some(ext) = path.extension() {
//hint.with_extension(&ext.to_string_lossy());
//}
//let meta_opts: MetadataOptions = Default::default();
//let fmt_opts: FormatOptions = Default::default();
//if let Ok(mut probed) = symphonia::default::get_probe()
//.format(&hint, mss, &fmt_opts, &meta_opts)
//{
//panic!("{:?}", probed.format.metadata());
//};
}
Ok(())
}
fn cursor_dir (&self) -> Option<PathBuf> {
if self.cursor < self.subdirs.len() {
Some(self.dir.join(&self.subdirs[self.cursor]))
} else {
None
}
}
fn cursor_file (&self) -> Option<PathBuf> {
if self.cursor < self.subdirs.len() {
return None
}
let index = self.cursor.saturating_sub(self.subdirs.len());
if index < self.files.len() {
Some(self.dir.join(&self.files[index]))
} else {
None
}
}
fn pick (&mut self) -> Usually<bool> {
if self.cursor == 0 {
if let Some(parent) = self.dir.parent() {
self.dir = parent.into();
self.rescan()?;
self.cursor = 0;
return Ok(false)
}
}
if let Some(dir) = self.cursor_dir() {
self.dir = dir;
self.rescan()?;
self.cursor = 0;
return Ok(false)
}
if let Some(path) = self.cursor_file() {
let (end, channels) = read_sample_data(&path.to_string_lossy())?;
let mut sample = self.sample.try_write().unwrap();
sample.name = path.file_name().unwrap().to_string_lossy().into();
sample.end = end;
sample.channels = channels;
return Ok(true)
}
return Ok(false)
}
}
#[allow(unused)]
fn read_sample_data (_: &str) -> Usually<(usize, Vec<Vec<f32>>)> {
todo!();
}

26
src/sample_kit.rs Normal file
View file

@ -0,0 +1,26 @@
use crate::*;
/// Collection of samples, one per slot, fixed number of slots.
///
/// History: Separated to cleanly implement [Default].
///
/// ```
/// let samples = tek::SampleKit([None, None, None, None]);
/// ```
#[derive(Debug)] pub struct SampleKit <const N: usize> (
pub [Option<Arc<RwLock<Sample>>>;N]
);
impl<const N: usize> Default for SampleKit<N> {
fn default () -> Self { Self([const { None }; N]) }
}
impl<const N: usize> SampleKit<N> {
pub fn get (&self, index: usize) -> &Option<Arc<RwLock<Sample>>> {
if index < self.0.len() {
&self.0[index]
} else {
&None
}
}
}

30
src/sample_voice.rs Normal file
View file

@ -0,0 +1,30 @@
use crate::*;
/// A currently playing instance of a sample.
#[derive(Default, Debug, Clone)] pub struct Voice {
pub sample: Arc<RwLock<Sample>>,
pub after: usize,
pub position: usize,
pub velocity: f32,
}
impl Iterator for Voice {
type Item = [f32;2];
fn next (&mut self) -> Option<Self::Item> {
if self.after > 0 {
self.after -= 1;
return Some([0.0, 0.0])
}
let sample = self.sample.try_read().unwrap();
if self.position < sample.end {
let position = self.position;
self.position += 1;
return sample.channels[0].get(position).map(|_amplitude|[
sample.channels[0][position] * self.velocity * sample.gain,
sample.channels[0][position] * self.velocity * sample.gain,
])
}
None
}
}

View file

@ -8,18 +8,6 @@ pub(crate) use symphonia::{
},
};
/// Get the first sampler of the active track
impl Project {
pub fn sampler (&self) -> Option<&Sampler> {
self.selected_track()?.sampler(0)
}
/// Get the first sampler of the active track
pub fn sampler_mut (&mut self) -> Option<&mut Sampler> {
self.selected_track_mut()?.sampler_mut(0)
}
}
pub trait HasSampler: AsRef<Sampler> + AsMut<Sampler> {
fn sampler (&self) -> &Sampler {
self.as_ref()
@ -441,327 +429,6 @@ fn draw_sample (
Ok(label1.len() + label2.len() + 4)
}
/// A currently playing instance of a sample.
#[derive(Default, Debug, Clone)] pub struct Voice {
pub sample: Arc<RwLock<Sample>>,
pub after: usize,
pub position: usize,
pub velocity: f32,
}
impl Iterator for Voice {
type Item = [f32;2];
fn next (&mut self) -> Option<Self::Item> {
if self.after > 0 {
self.after -= 1;
return Some([0.0, 0.0])
}
let sample = self.sample.try_read().unwrap();
if self.position < sample.end {
let position = self.position;
self.position += 1;
return sample.channels[0].get(position).map(|_amplitude|[
sample.channels[0][position] * self.velocity * sample.gain,
sample.channels[0][position] * self.velocity * sample.gain,
])
}
None
}
}
/// Collection of samples, one per slot, fixed number of slots.
///
/// History: Separated to cleanly implement [Default].
///
/// ```
/// let samples = tek::SampleKit([None, None, None, None]);
/// ```
#[derive(Debug)] pub struct SampleKit <const N: usize> (
pub [Option<Arc<RwLock<Sample>>>;N]
);
impl<const N: usize> Default for SampleKit<N> {
fn default () -> Self { Self([const { None }; N]) }
}
impl<const N: usize> SampleKit<N> {
pub fn get (&self, index: usize) -> &Option<Arc<RwLock<Sample>>> {
if index < self.0.len() {
&self.0[index]
} else {
&None
}
}
}
/// A sound cut.
///
/// ```
/// let sample = tek::Sample::default();
/// let sample = tek::Sample::new("test", 0, 0, vec![]);
/// ```
#[derive(Default, Debug)] pub struct Sample {
pub name: Arc<str>,
pub start: usize,
pub end: usize,
pub channels: Vec<Vec<f32>>,
pub rate: Option<usize>,
pub gain: f32,
pub color: ItemTheme,
}
impl Sample {
pub fn new (name: impl AsRef<str>, start: usize, end: usize, channels: Vec<Vec<f32>>) -> Self {
Self {
name: name.as_ref().into(),
start,
end,
channels,
rate: None,
gain: 1.0,
color: ItemTheme::random(),
}
}
pub fn play (sample: &Arc<RwLock<Self>>, after: usize, velocity: &u7) -> Voice {
Voice {
sample: sample.clone(),
after,
position: sample.try_read().unwrap().start,
velocity: velocity.as_int() as f32 / 127.0,
}
}
pub fn handle_cc (&mut self, controller: u7, value: u7) {
let percentage = value.as_int() as f64 / 127.;
match controller.as_int() {
20 => {
self.start = (percentage * self.end as f64) as usize;
},
21 => {
let length = self.channels[0].len();
self.end = length.min(
self.start + (percentage * (length as f64 - self.start as f64)) as usize
);
},
22 => { /*attack*/ },
23 => { /*decay*/ },
24 => {
self.gain = percentage as f32 * 2.0;
},
26 => { /* pan */ }
25 => { /* pitch */ }
_ => {}
}
}
/// Read WAV from file
pub fn read_data (src: &str) -> Usually<(usize, Vec<Vec<f32>>)> {
let mut channels: Vec<wavers::Samples<f32>> = vec![];
for channel in wavers::Wav::from_path(src)?.channels() {
channels.push(channel);
}
let mut end = 0;
let mut data: Vec<Vec<f32>> = vec![];
for samples in channels.iter() {
let channel = Vec::from(samples.as_ref());
end = end.max(channel.len());
data.push(channel);
}
Ok((end, data))
}
pub fn from_file (path: &PathBuf) -> Usually<Self> {
let name = path.file_name().unwrap().to_string_lossy().into();
let mut sample = Self { name, ..Default::default() };
// Use file extension if present
let mut hint = Hint::new();
if let Some(ext) = path.extension() {
hint.with_extension(&ext.to_string_lossy());
}
let probed = symphonia::default::get_probe().format(
&hint,
MediaSourceStream::new(
Box::new(File::open(path)?),
Default::default(),
),
&Default::default(),
&Default::default()
)?;
let mut format = probed.format;
let params = &format.tracks().iter()
.find(|t| t.codec_params.codec != CODEC_TYPE_NULL)
.expect("no tracks found")
.codec_params;
let mut decoder = get_codecs().make(params, &Default::default())?;
loop {
match format.next_packet() {
Ok(packet) => sample.decode_packet(&mut decoder, packet)?,
Err(symphonia::core::errors::Error::IoError(_)) => break decoder.last_decoded(),
Err(err) => return Err(err.into()),
};
};
sample.end = sample.channels.iter().fold(0, |l, c|l + c.len());
Ok(sample)
}
fn decode_packet (
&mut self, decoder: &mut Box<dyn Decoder>, packet: Packet
) -> Usually<()> {
// Decode a packet
let decoded = decoder
.decode(&packet)
.map_err(|e|Box::<dyn std::error::Error>::from(e))?;
// Determine sample rate
let spec = *decoded.spec();
if let Some(rate) = self.rate {
if rate != spec.rate as usize {
panic!("sample rate changed");
}
} else {
self.rate = Some(spec.rate as usize);
}
// Determine channel count
while self.channels.len() < spec.channels.count() {
self.channels.push(vec![]);
}
// Load sample
let mut samples = SampleBuffer::new(
decoded.frames() as u64,
spec
);
if samples.capacity() > 0 {
samples.copy_interleaved_ref(decoded);
for frame in samples.samples().chunks(spec.channels.count()) {
for (chan, frame) in frame.iter().enumerate() {
self.channels[chan].push(*frame)
}
}
}
Ok(())
}
}
#[derive(Default, Debug)] pub struct SampleAdd {
pub exited: bool,
pub dir: PathBuf,
pub subdirs: Vec<OsString>,
pub files: Vec<OsString>,
pub cursor: usize,
pub offset: usize,
pub sample: Arc<RwLock<Sample>>,
pub voices: Arc<RwLock<Vec<Voice>>>,
pub _search: Option<String>,
}
impl_draw!(|self: SampleAdd, to: Tui|{ todo!() });
impl SampleAdd {
fn exited (&self) -> bool {
self.exited
}
fn exit (&mut self) {
self.exited = true
}
pub fn new (
sample: &Arc<RwLock<Sample>>,
voices: &Arc<RwLock<Vec<Voice>>>
) -> Usually<Self> {
let dir = std::env::current_dir()?;
let (subdirs, files) = scan(&dir)?;
Ok(Self {
exited: false,
dir,
subdirs,
files,
cursor: 0,
offset: 0,
sample: sample.clone(),
voices: voices.clone(),
_search: None
})
}
fn rescan (&mut self) -> Usually<()> {
scan(&self.dir).map(|(subdirs, files)|{
self.subdirs = subdirs;
self.files = files;
})
}
fn prev (&mut self) {
self.cursor = self.cursor.saturating_sub(1);
}
fn next (&mut self) {
self.cursor = self.cursor + 1;
}
fn try_preview (&mut self) -> Usually<()> {
if let Some(path) = self.cursor_file() {
if let Ok(sample) = Sample::from_file(&path) {
*self.sample.try_write().unwrap() = sample;
self.voices.try_write().unwrap().push(
Sample::play(&self.sample, 0, &u7::from(100u8))
);
}
//load_sample(&path)?;
//let src = std::fs::File::open(&path)?;
//let mss = MediaSourceStream::new(Box::new(src), Default::default());
//let mut hint = Hint::new();
//if let Some(ext) = path.extension() {
//hint.with_extension(&ext.to_string_lossy());
//}
//let meta_opts: MetadataOptions = Default::default();
//let fmt_opts: FormatOptions = Default::default();
//if let Ok(mut probed) = symphonia::default::get_probe()
//.format(&hint, mss, &fmt_opts, &meta_opts)
//{
//panic!("{:?}", probed.format.metadata());
//};
}
Ok(())
}
fn cursor_dir (&self) -> Option<PathBuf> {
if self.cursor < self.subdirs.len() {
Some(self.dir.join(&self.subdirs[self.cursor]))
} else {
None
}
}
fn cursor_file (&self) -> Option<PathBuf> {
if self.cursor < self.subdirs.len() {
return None
}
let index = self.cursor.saturating_sub(self.subdirs.len());
if index < self.files.len() {
Some(self.dir.join(&self.files[index]))
} else {
None
}
}
fn pick (&mut self) -> Usually<bool> {
if self.cursor == 0 {
if let Some(parent) = self.dir.parent() {
self.dir = parent.into();
self.rescan()?;
self.cursor = 0;
return Ok(false)
}
}
if let Some(dir) = self.cursor_dir() {
self.dir = dir;
self.rescan()?;
self.cursor = 0;
return Ok(false)
}
if let Some(path) = self.cursor_file() {
let (end, channels) = read_sample_data(&path.to_string_lossy())?;
let mut sample = self.sample.try_write().unwrap();
sample.name = path.file_name().unwrap().to_string_lossy().into();
sample.end = end;
sample.channels = channels;
return Ok(true)
}
return Ok(false)
}
}
fn read_sample_data (_: &str) -> Usually<(usize, Vec<Vec<f32>>)> {
todo!();
}
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();
@ -809,6 +476,18 @@ pub fn view_sample_status (sample: Option<&Arc<RwLock<Sample>>>) -> impl Draw<Tu
.unwrap_or_else(||"No sample".to_string())))
}
/// Get the first sampler of the active track
impl Project {
pub fn sampler (&self) -> Option<&Sampler> {
self.selected_track()?.sampler(0)
}
/// Get the first sampler of the active track
pub fn sampler_mut (&mut self) -> Option<&mut Sampler> {
self.selected_track_mut()?.sampler_mut(0)
}
}
#[cfg(feature = "track")]
impl Track {
/// Create a new track connecting the [Sequencer] to a [Sampler].

View file

@ -55,10 +55,6 @@ impl Scene {
}
impl_as_ref_opt!(Scene: |self: App| self.project.as_ref_opt());
impl_as_mut_opt!(Scene: |self: App| self.project.as_mut_opt());
impl_as_ref_opt!(Scene: |self: Project| self.selected_scene());
impl_as_mut_opt!(Scene: |self: Project| self.selected_scene_mut());
impl<T: AsRefOpt<Scene> + AsMutOpt<Scene> + Send + Sync> HasScene for T {}
@ -123,9 +119,10 @@ pub trait SceneController: HasScene
pub type SceneWith<'a, T> = (usize, &'a Scene, usize, usize, T);
def_sizes_iter!(ScenesSizes => Scene);
impl_has!(Vec<Scene>: |self: Project| self.scenes);
impl_as_ref!(Vec<Scene>: |self: App| self.project.as_ref());
impl_as_mut!(Vec<Scene>: |self: App| self.project.as_mut());
impl_as_ref_opt!(Scene: |self: App| self.project.as_ref_opt());
impl_as_mut_opt!(Scene: |self: App| self.project.as_mut_opt());
impl<T: AsRef<Vec<Scene>> + AsMut<Vec<Scene>>> HasScenes for T {}
@ -236,7 +233,7 @@ pub trait ScenesView: HasEditor + HasSelection + HasSceneScroll + HasClipsSize +
let editor = self.editor();
let editing = self.is_editing();
iter_south(move||self.scenes_with_sizes().map(move|(index, scene, ..)|{
view_scene_name(select, editor, index, scene, editing)
view_scene_name(select, editor.as_ref(), index, scene, editing)
}))
}
@ -322,3 +319,51 @@ 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)
}

View file

@ -43,7 +43,7 @@
(bar/n 2 :status
(bar/s 8
(bar/e 16 (align/nw (pad/x 1 :transport))
(bar/w 16 (bar/s 2 (bar/w 4 :tracks/outputs/add :tracks/outputs/count)
(bar/w 20 (bar/s 2 (bar/w 4 :tracks/outputs/add :tracks/outputs/count)
(bar/s 2 (bar/w 4 :tracks/devices/add :tracks/devices/count)
(bar/s 2 (bar/w 4 :tracks/names/add :tracks/names/count)
(bar/w 4 :tracks/inputs/add :tracks/inputs/count))))
@ -52,7 +52,7 @@
(bar/s 2 :tracks/devices/cells
(bar/s 2 :tracks/names/cells
:tracks/inputs/cells))))))
(bar/w 16 (text)
(bar/w 20 (text)
(bar/e 16 :scenes/names
:scenes/clips))))))
@ -89,7 +89,7 @@
(keys :clip (see :color :launch :axis/z :axis/z2 :delete)
(@l (toggle :loop))
(@tab (toggle :edit)))
(@tab (clip upsert) (edit toggle)))
(view :ports/out
(bsp/s (align/w (text L-AUDIO-OUT))

View file

@ -9,26 +9,22 @@ macro_rules! def_sizes_iter {
}
mod app; pub use self::app::*;
mod bind; pub use self::bind::*;
mod binds; pub use self::binds::*;
mod browse; pub use self::browse::*;
mod cli; pub use self::cli::*;
mod clip; pub use self::clip::*;
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 editor; pub use self::editor::*;
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 mode; pub use self::mode::*;
mod mode_stack; pub use self::mode_stack::*;
mod modes; pub use self::modes::*;
mod moment; pub use self::moment::*;
mod octave; pub use self::octave::*;
mod piano; pub use self::piano::*;
@ -38,10 +34,15 @@ 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::*;
@ -49,7 +50,6 @@ 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 views; pub use self::views::*;
#[cfg(feature = "lv2")] mod lv2;
#[cfg(feature = "lv2")] pub use self::lv2::*;

View file

@ -352,7 +352,7 @@ pub trait TracksView: HasTracks + ScenesView + HasMidiIns + HasMidiOuts + HasTra
/// Iterate over tracks with their corresponding sizes.
fn tracks_with_sizes (&self) -> impl TracksSizes<'_> {
let _editor_width = self.editor().map(|e|e.size.w());
let _editor_width = self.editor().as_ref().map(|e|e.size.w());
let _active_track = self.selection().track();
let mut x = 0;
let w = self.clips_size().w() as usize;

2
tengri

@ -1 +1 @@
Subproject commit a8ee66a8cbdc33eb162b31b8da36b4ec203fd5ff
Subproject commit 79f9cfcbf93db626d031ab86abbbdf5c29d80a48