mirror of
https://codeberg.org/unspeaker/tek.git
synced 2026-10-08 18:56:42 +02:00
1797 lines
74 KiB
Rust
1797 lines
74 KiB
Rust
use crate::*;
|
||
|
||
/// 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 |_|{
|
||
draw_error(to, self.error.as_ref())?;
|
||
draw_mode_stack(to, &self)?;
|
||
view_debug(&self.perf, &self.size).draw(to)?;
|
||
#[cfg(feature = "prof2")] profiling::finish_frame!();
|
||
Ok(Some(to.area().into()))
|
||
})
|
||
}
|
||
}
|
||
|
||
pub fn draw_error (to: &mut Tui, error: &RwLock<Option<Arc<str>>>) -> Usually<()> {
|
||
if let Some(e) = error.read().as_ref() {
|
||
e.as_ref().align_c().draw(to)?;
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
pub fn view_debug (perf: &PerfModel, size: &Sizer) -> impl Draw<Tui> {
|
||
Split::Above.bar(0,
|
||
Split::West.bar(16,
|
||
format!("{}/{} {} ",
|
||
perf.used.load(Relaxed),
|
||
perf.window.load(Relaxed),
|
||
perf.clock.raw() / 10000000).align_e(),
|
||
format!("{}x{} ",
|
||
size.0.load(Relaxed),
|
||
size.1.load(Relaxed)).align_w(),
|
||
).align_se(),
|
||
()
|
||
//Split::East.bar(16,
|
||
//format!("{}", self.modes.read()[0]),
|
||
//format!("{}", self.modes.read()[0])
|
||
//).align_sw()
|
||
)
|
||
}
|
||
|
||
pub fn draw_mode_stack (to: &mut Tui, state: &App) -> Usually<()> {
|
||
for name in state.modes.read().iter() {
|
||
let mut error = false;
|
||
if let Some(mode) = state.config.get_mode(name).as_ref().map(Arc::clone) {
|
||
for (index, view) in mode.view.iter().enumerate() {
|
||
match (view.render)(state, to) {
|
||
Ok(None) => {},
|
||
Ok(Some(XYWH(.., w, h))) => {
|
||
state.size.0.store(w as usize, Relaxed);
|
||
state.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
|
||
);
|
||
*state.error.try_write().unwrap() = Some(message.into());
|
||
error = true;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
if error {
|
||
break
|
||
} else {
|
||
*state.error.try_write().unwrap() = None;
|
||
}
|
||
}
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
/// Collection of custom view definitions.
|
||
#[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> (&self, expr: impl Language) -> UsuallyRef<'a, ()> {
|
||
self.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> {
|
||
pub source: Arc<str>,
|
||
pub render: Arc<Box<dyn Fn(&T, &mut S)->Drawn<S::Unit> + Send + Sync>>
|
||
}
|
||
|
||
impl_debug!(<S: Screen, T> View<S, T> |self, w| { write!(w, "View({})", self.source) });
|
||
|
||
impl_display!(<S: Screen, T> View<S, T> |self, w| { write!(w, "View({})", self.source) });
|
||
|
||
impl View<Tui, App> {
|
||
|
||
pub fn new (source: impl AsRef<str>) -> Usually<Self> {
|
||
Ok(Self {
|
||
source: source.as_ref().into(),
|
||
render: Self::compile(source)?
|
||
})
|
||
}
|
||
|
||
fn boxed <F: Fn(&App, &mut Tui)->Drawn<u16> + Send + Sync + 'static> (f: F)
|
||
-> Box<dyn Fn(&App, &mut Tui)->Drawn<u16> + Send + Sync + 'static>
|
||
{
|
||
Box::new(f)
|
||
}
|
||
|
||
fn compile (source: impl AsRef<str>) ->
|
||
Usually<Arc<Box<dyn Fn(&App, &mut Tui)->Drawn<u16> + Send + Sync>>>
|
||
{
|
||
let source = source.as_ref();
|
||
let layer = if let Ok(Some(expr)) = source.expr() {
|
||
Self::compile_expr(expr.into())?
|
||
} else if let Ok(Some(word)) = source.word() {
|
||
Self::compile_word(word.into())?
|
||
} else {
|
||
return err!("not word/expr:\n{source:?}")
|
||
};
|
||
Ok(Arc::new(Box::new(move|state, screen|layer(state, screen))))
|
||
}
|
||
|
||
fn compile_expr (expr: Arc<str>) ->
|
||
Usually<Arc<Box<dyn Fn(&App, &mut Tui)->Drawn<u16> + Send + Sync>>>
|
||
{
|
||
|
||
macro_rules! arg {
|
||
($src:expr, $head:expr, $index:expr, $name:expr) => {
|
||
$src.nth($index)?
|
||
.ok_or_else(||Box::<dyn Error>::from(format!(
|
||
"{}: no arg #{} ({}) in {}", $head, $index, $name, $src
|
||
)))?
|
||
};
|
||
}
|
||
|
||
Ok(Arc::new(if let Some(head) = expr.head()? && let Some(ns) = head.split('/').next() {
|
||
|
||
match ns {
|
||
|
||
"when" => {
|
||
let cond = Arc::from(arg!(expr, head, 1, "condition"));
|
||
let cond = move|state: &App|state.namespace(&cond)?.ok_or_else(||Box::<dyn Error>::from("when: no condition value"));
|
||
let thunk = Self::compile(arg!(expr, head, 2, "content"))?;
|
||
Self::boxed(move|state, screen|{
|
||
when(
|
||
cond(state)?,
|
||
draw(|screen|thunk(state, screen))
|
||
).draw(screen)
|
||
})
|
||
},
|
||
|
||
"either" => {
|
||
let cond = Arc::from(arg!(expr, head, 1, "condition"));
|
||
let cond = move|state: &App|state.namespace(&cond)?.ok_or_else(||Box::<dyn Error>::from("either: no condition value"));
|
||
let a = Self::compile(arg!(expr, head, 2, "content A"))?;
|
||
let b = Self::compile(arg!(expr, head, 3, "content B"))?;
|
||
Self::boxed(move|state, screen|{
|
||
either(
|
||
cond(state)?,
|
||
draw(|screen|a(state, screen)),
|
||
draw(|screen|b(state, screen)),
|
||
).draw(screen)
|
||
})
|
||
},
|
||
|
||
"bsp" | "split" | "stack" => {
|
||
let split = head.split('/').skip(1).next();
|
||
let split = match split {
|
||
Some("n") => Split::North,
|
||
Some("s") => Split::South,
|
||
Some("e") => Split::East,
|
||
Some("w") => Split::West,
|
||
Some("a") => Split::Above,
|
||
Some("b") => Split::Below,
|
||
_ => 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"))?;
|
||
Self::boxed(move|state, screen|split.stack(
|
||
draw(|screen|a(state, screen)),
|
||
draw(|screen|b(state, screen)),
|
||
).draw(screen))
|
||
},
|
||
|
||
"bar" => {
|
||
let split = head.split('/').skip(1).next();
|
||
let split = match split {
|
||
Some("n") => Split::North,
|
||
Some("s") => Split::South,
|
||
Some("e") => Split::East,
|
||
Some("w") => Split::West,
|
||
Some("a") => Split::Above,
|
||
Some("b") => Split::Below,
|
||
_ => 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"));
|
||
let a = Self::compile(arg!(expr, head, 2, "content A"))?;
|
||
let b = Self::compile(arg!(expr, head, 3, "content B"))?;
|
||
Self::boxed(move|state, screen|split.bar(
|
||
size(state)?,
|
||
draw(|screen|a(state, screen)),
|
||
draw(|screen|b(state, screen)),
|
||
).draw(screen))
|
||
},
|
||
|
||
"align" => {
|
||
let azimuth = head.split('/').skip(1).next();
|
||
let azimuth = match azimuth {
|
||
Some("n") => Azimuth::N,
|
||
Some("s") => Azimuth::S,
|
||
Some("e") => Azimuth::E,
|
||
Some("w") => Azimuth::W,
|
||
Some("ne") => Azimuth::NE,
|
||
Some("se") => Azimuth::SE,
|
||
Some("nw") => Azimuth::NW,
|
||
Some("sw") => Azimuth::SW,
|
||
Some("c") => Azimuth::C,
|
||
Some("x") => Azimuth::X,
|
||
Some("y") => Azimuth::Y,
|
||
_ => return err!("invalid azimuth: {azimuth:?}")
|
||
};
|
||
let thunk = Self::compile(arg!(expr, head, 1, "content"))?;
|
||
Self::boxed(move|state, screen|{
|
||
Align(
|
||
Some(azimuth),
|
||
draw(|screen|thunk(state, screen))
|
||
).draw(screen)
|
||
})
|
||
},
|
||
|
||
"full" | "fill" => {
|
||
let thunk = Self::compile(arg!(expr, head, 1, "content"))?;
|
||
match head.split('/').skip(1).next() {
|
||
Some("w") | Some("x") => Self::boxed(move|state, screen|{
|
||
Full::W(draw(|screen|thunk(state, screen))).draw(screen)
|
||
}),
|
||
Some("h") | Some("y") => Self::boxed(move|state, screen|{
|
||
Full::H(draw(|screen|thunk(state, screen))).draw(screen)
|
||
}),
|
||
Some("wh") | Some("xy") | None => Self::boxed(move|state, screen|{
|
||
Full::WH(draw(|screen|thunk(state, screen))).draw(screen)
|
||
}),
|
||
_ => unreachable!()
|
||
}
|
||
},
|
||
|
||
"exact" | "min" | "max" | "push" | "pull" | "pad" | "shrink" | "expand" => {
|
||
match head.split('/').skip(1).next() {
|
||
Some("w") | Some("x") => {
|
||
let value = Arc::from(arg!(expr, head, 1, "value"));
|
||
let value = move|state: &App|state.namespace(&value);
|
||
let thunk = Self::compile(arg!(expr, head, 2, "content"))?;
|
||
match ns {
|
||
"shrink" => Self::boxed(move|state, screen|Shrink::W(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)),
|
||
"expand" => Self::boxed(move|state, screen|Expand::W(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)),
|
||
"exact" => Self::boxed(move|state, screen|Exact::W(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)),
|
||
"push" => Self::boxed(move|state, screen|Push::X(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)),
|
||
"pull" => Self::boxed(move|state, screen|Pull::X(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)),
|
||
"pad" => Self::boxed(move|state, screen|Pad::X(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)),
|
||
"min" => Self::boxed(move|state, screen|Min::W(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)),
|
||
"max" => Self::boxed(move|state, screen|Max::W(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)),
|
||
_ => unreachable!()
|
||
}
|
||
},
|
||
Some("h") | Some("y") => {
|
||
let value = Arc::from(arg!(expr, head, 1, "value"));
|
||
let value = move|state: &App|state.namespace(&value);
|
||
let thunk = Self::compile(arg!(expr, head, 2, "content"))?;
|
||
match ns {
|
||
"shrink" => Self::boxed(move|state, screen|Shrink::H(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)),
|
||
"expand" => Self::boxed(move|state, screen|Expand::H(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)),
|
||
"exact" => Self::boxed(move|state, screen|Exact::H(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)),
|
||
"push" => Self::boxed(move|state, screen|Push::Y(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)),
|
||
"pull" => Self::boxed(move|state, screen|Pull::Y(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)),
|
||
"pad" => Self::boxed(move|state, screen|Pad::Y(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)),
|
||
"min" => Self::boxed(move|state, screen|Min::H(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)),
|
||
"max" => Self::boxed(move|state, screen|Max::H(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)),
|
||
_ => unreachable!()
|
||
}
|
||
},
|
||
Some("wh") | Some("xy") | None => {
|
||
let value1 = Arc::from(arg!(expr, head, 1, "value"));
|
||
let value1 = move|state: &App|state.namespace(&value1);
|
||
let value2 = Arc::from(arg!(expr, head, 2, "value"));
|
||
let value2 = move|state: &App|state.namespace(&value2);
|
||
let thunk = Self::compile(arg!(expr, head, 3, "content"))?;
|
||
match ns {
|
||
"shrink" => Self::boxed(move|state, screen|Shrink::WH(draw(|screen|thunk(state, screen)), value1(state)?, value2(state)?).draw(screen)),
|
||
"expand" => Self::boxed(move|state, screen|Expand::WH(draw(|screen|thunk(state, screen)), value1(state)?, value2(state)?).draw(screen)),
|
||
"exact" => Self::boxed(move|state, screen|Exact::WH(draw(|screen|thunk(state, screen)), value1(state)?, value2(state)?).draw(screen)),
|
||
"push" => Self::boxed(move|state, screen|Push::XY(draw(|screen|thunk(state, screen)), value1(state)?, value2(state)?).draw(screen)),
|
||
"pull" => Self::boxed(move|state, screen|Pull::XY(draw(|screen|thunk(state, screen)), value1(state)?, value2(state)?).draw(screen)),
|
||
"pad" => Self::boxed(move|state, screen|Pad::XY(draw(|screen|thunk(state, screen)), value1(state)?, value2(state)?).draw(screen)),
|
||
"min" => Self::boxed(move|state, screen|Min::WH(draw(|screen|thunk(state, screen)), value1(state)?, value2(state)?).draw(screen)),
|
||
"max" => Self::boxed(move|state, screen|Max::WH(draw(|screen|thunk(state, screen)), value1(state)?, value2(state)?).draw(screen)),
|
||
_ => unreachable!()
|
||
}
|
||
},
|
||
_ => unreachable!()
|
||
}
|
||
},
|
||
|
||
"fg" | "bg" => {
|
||
let color = Arc::from(arg!(expr, head, 1, "color"));
|
||
let color = move|state: &App|state.namespace(&color);
|
||
let thunk = Self::compile(arg!(expr, head, 2, "content"))?;
|
||
match ns {
|
||
"fg" => Self::boxed(move|state, screen|fg(
|
||
color(state)?.unwrap_or_default(), draw(|screen|thunk(state, screen))
|
||
).draw(screen)),
|
||
"bg" => Self::boxed(move|state, screen|bg(
|
||
color(state)?.unwrap_or_default(), draw(|screen|thunk(state, screen))
|
||
).draw(screen)),
|
||
_ => unreachable!()
|
||
}
|
||
},
|
||
|
||
"text" => {
|
||
let text: Arc<str> = Arc::from(expr.tail()?.unwrap_or_default());
|
||
Self::boxed(move|_state, screen|{
|
||
text.draw(screen)
|
||
})
|
||
},
|
||
|
||
_ => return err!("compile_expr: unexpected: {expr:?}")
|
||
|
||
}
|
||
|
||
} else {
|
||
return err!("compile_expr: invalid expression: {expr:?}")
|
||
}))
|
||
}
|
||
|
||
fn compile_word (word: Arc<str>)
|
||
-> Usually<Arc<Box<dyn Fn(&App, &mut Tui)->Drawn<u16> + Send + Sync>>>
|
||
{
|
||
macro_rules! view {
|
||
(|$state:ident|$body:expr)=>{Box::new(move|$state, to|$body.draw(to))};
|
||
($const:expr)=>{Box::new(move|_, to|$const.draw(to))};
|
||
()=>{Box::new(move|_, __|Ok(None))};
|
||
}
|
||
|
||
Ok(Arc::new(match word.split("/").next() {
|
||
Some("Beat") => view!(),
|
||
Some("Bpm") => view!(),
|
||
Some("BrowseTitle") => view!(|app|view_browse_title(app)),
|
||
Some("Config") => view!(|app|view_config(app)),
|
||
Some("Debug") => Box::new(move|_, to|format!("[{:?}]", to.area()).exact_h(1).draw(to)),
|
||
Some("Device") => view!(|app|view_device(app)),
|
||
Some("Menu") => view!(|app|view_menu(app)),
|
||
Some("Editor") => view!(|app|app.editor()),
|
||
Some("MetersIn") => view!(|___|bg(Rgb(30, 30, 30), "Input Meters".align_s().full_h())),
|
||
Some("MetersOut") => view!(|___|bg(Rgb(30, 30, 30), "Output Meters".align_s().full_h())),
|
||
Some("ModeList") => view!(|app|view_modes(app)),
|
||
Some("HistoryCount") => view!(|app|app.history.len()),
|
||
Some("HistoryList") => view!(|app|view_history_list(app)),
|
||
Some("ScenesClips") => view!(|app|view_scenes_clips(app)),
|
||
Some("ScenesNames") => view!(|app|app.view_scenes_names()),
|
||
Some("Sessions") => view!(|___|view_sessions()),
|
||
Some("Selected") => view!(|app|view_selection(app)),
|
||
Some("Templates") => view!(|app|view_templates(app)),
|
||
Some("Time") => view!(),
|
||
Some("Tracks") => view!("TODO tracks"),
|
||
Some("TracksDevices") => view!(|app|app.project.view_track_devices(app.color.clone())),
|
||
Some("TracksDevicesAdd") => view!(|app|app.project.view_track_device_add()),
|
||
Some("TracksDevicesCells") => view!(|app|app.project.view_track_device_cells(app.tracks_devices_height())),
|
||
Some("TracksDevicesCount") => view!(|app|app.project.view_track_device_count()),
|
||
Some("TracksInputs") => view!(|app|app.project.view_track_inputs(app.color.clone())),
|
||
Some("TracksInputsAdd") => view!(|app|app.project.view_track_input_add()),
|
||
Some("TracksInputsCells") => view!(|app|app.project.view_track_input_cells(app.tracks_inputs_height())),
|
||
Some("TracksInputsCount") => view!(|app|app.project.view_track_input_count()),
|
||
Some("TracksNames") => view!(|app|app.project.view_track_names(app.color.clone())),
|
||
Some("TracksNamesAdd") => view!(|app|app.project.view_track_names_add()),
|
||
Some("TracksNamesCells") => view!(|app|app.project.view_track_names_cells()),
|
||
Some("TracksNamesCount") => view!(|app|app.project.view_track_names_count()),
|
||
Some("TracksOutputs") => view!(|app|app.project.view_track_outputs(app.color.clone(), 0)),
|
||
Some("TracksOutputsAdd") => view!(|app|app.project.view_track_output_add()),
|
||
Some("TracksOutputsCells") => view!(|app|app.project.view_track_output_cells()),
|
||
Some("TracksOutputsCount") => view!(|app|app.project.view_track_output_count()),
|
||
Some(_) => Box::new(move|state, to|if let Some(view) = state.config.get_view(word.as_ref()) {
|
||
(view.render)(state, to)
|
||
} else {
|
||
fg(Color::Rgb(128, 32, 32), format!("undefined: {word:?}")).draw(to)
|
||
}),
|
||
_ => unreachable!()
|
||
}))
|
||
}
|
||
}
|
||
|
||
pub fn view_menu (app: &App) -> impl Draw<Tui> {
|
||
if let Dialog::Menu(selected, items) = &app.dialog {
|
||
Some(iter_south(move||items.iter().enumerate().map(move|(index, MenuItem(item, _))|{
|
||
let f = if *selected == index { Rgb(240,200,180) } else { Rgb(200, 200, 200) };
|
||
let b = if *selected == index { Rgb(80, 80, 50) } else { Rgb(30, 30, 30) };
|
||
fg_bg(f, b, item).exact_h(2).align_x()
|
||
})).exact_w(20).align_x())
|
||
} else {
|
||
None
|
||
}
|
||
}
|
||
|
||
pub fn view_history_list (app: &App) -> impl Draw<Tui> {
|
||
above(
|
||
Outer(true, Style::default().fg(Color::Rgb(0, 192, 0))),
|
||
draw(|to: &mut Tui|{
|
||
let area = to.area();
|
||
let XYWH(x0, mut y0, w, h) = area;
|
||
for (done, undo) in app.history.iter().rev() {
|
||
to.clip(XYWH(x0 + 1, y0, w, 1), &|to|format!("{done:?}").draw(to))?;
|
||
y0 += 1;
|
||
}
|
||
Ok(Some(area))
|
||
})
|
||
)
|
||
}
|
||
|
||
pub fn view_config (app: &App) -> impl Draw<Tui> {
|
||
above(
|
||
Outer(true, Style::default().fg(Color::Rgb(0, 192, 0))),
|
||
draw(|to: &mut Tui|{
|
||
let area = to.area();
|
||
let XYWH(x0, mut y0, w, h) = area;
|
||
for key in app.modes.read().iter().rev() {
|
||
if let Some(mode) = app.config.modes.get(key) {
|
||
to.clip(XYWH(x0 + 1, y0, w, 1), &|to|Split::East.bar(16, key, mode.info.get(0)).draw(to))?;
|
||
y0 += 1;
|
||
for bind in mode.keys.iter() {
|
||
to.clip(XYWH(x0 + 3, y0, w, 1), &|to|bind.draw(to))?;
|
||
y0 += 1;
|
||
if let Some(bind) = app.config.binds.find(bind) {
|
||
for (key, binding) in bind.0.iter() {
|
||
to.clip(XYWH(x0 + 5, y0, 40, 1),
|
||
&|to|Split::East.bar(20, format!("{:?}", key.0), "").draw(to))?;
|
||
y0 += 1;
|
||
}
|
||
}
|
||
}
|
||
y0 += 1;
|
||
}
|
||
}
|
||
//for (index, (key, mode)) in app.config.modes.0.read().iter().enumerate() {
|
||
//to.clip(XYWH(x0, y0 + index as u16, w, 1), &|to|Split::East.bar(16,
|
||
//key, mode.info.get(0),
|
||
//).draw(to))?;
|
||
//}
|
||
Ok(Some(area))
|
||
})
|
||
)
|
||
}
|
||
|
||
pub fn view_config_mode (mode: &Mode) -> impl Draw<Tui> {
|
||
}
|
||
|
||
/// ```
|
||
/// let _ = tek::button_play_pause(true, true);
|
||
/// let _ = tek::button_play_pause(true, false);
|
||
/// let _ = tek::button_play_pause(false, true);
|
||
/// let _ = tek::button_play_pause(false, false);
|
||
/// ```
|
||
pub fn button_play_pause <'a> (playing: bool, compact: bool) -> impl Draw<Tui> {
|
||
bg(if playing { Rgb(0, 128, 0) } else { Rgb(128, 64, 0) },
|
||
either(compact,
|
||
draw(move|to: &mut Tui|either(playing,
|
||
fg(Rgb(0, 255, 0), " PLAYING "),
|
||
fg(Rgb(255, 128, 0), " STOPPED "),
|
||
).exact_w(9).draw(to)),
|
||
draw(move|to: &mut Tui|either(playing,
|
||
fg(Rgb(0, 255, 0), south(" 🭍🭑🬽 ", " 🭞🭜🭘 ",)),
|
||
fg(Rgb(255, 128, 0), south(" ▗▄▖ ", " ▝▀▘ ",)),
|
||
).exact_w(5).draw(to)),
|
||
)
|
||
)
|
||
}
|
||
|
||
impl Draw<Tui> for PianoHorizontal {
|
||
fn draw (&self, to: &mut Tui) -> Drawn<u16> {
|
||
south(
|
||
east(
|
||
format!("{}x{}", self.size.w(), self.size.h()).exact_w(5),
|
||
view_piano_h_timeline(self)
|
||
),
|
||
east(
|
||
view_piano_h_keys(self),
|
||
self.size.of(below(
|
||
view_piano_h_notes(self).full_wh(),
|
||
view_piano_h_cursor(self).full_wh()
|
||
))
|
||
),
|
||
).draw(to)
|
||
}
|
||
}
|
||
|
||
pub fn view_piano_h_notes (state: &PianoHorizontal) -> impl Draw<Tui> {
|
||
let time_start = state.get_time_start();
|
||
let note_lo = state.get_note_lo();
|
||
let note_hi = state.get_note_hi();
|
||
let buffer = state.buffer.clone();
|
||
draw(move|to: &mut Tui|{
|
||
let xywh = to.area().into();
|
||
to.buffer().map(|to|{
|
||
let XYWH(x0, y0, w, _h) = xywh;
|
||
let source = buffer.read();
|
||
//if h as usize != note_axis {
|
||
//panic!("area height mismatch: {h} <> {note_axis}");
|
||
//}
|
||
for (area_x, screen_x) in (x0..x0+w).enumerate() {
|
||
for (area_y, screen_y, _note) in note_y_iter(note_lo, note_hi, y0) {
|
||
let source_x = time_start + area_x;
|
||
let source_y = note_hi - area_y;
|
||
// TODO: enable loop rollover:
|
||
//let source_x = (time_start + area_x) % source.width.max(1);
|
||
//let source_y = (note_hi - area_y) % source.height.max(1);
|
||
let is_in_x = source_x < source.width;
|
||
let is_in_y = source_y < source.height;
|
||
if is_in_x && is_in_y {
|
||
if let Some(source_cell) = source.get(source_x, source_y) {
|
||
if let Some(cell) = to.cell_mut(
|
||
ratatui::prelude::Position::from((screen_x, screen_y))
|
||
) {
|
||
*cell = source_cell.clone();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
});
|
||
Ok(Some(xywh))
|
||
})
|
||
}
|
||
|
||
pub fn view_piano_h_cursor (state: &PianoHorizontal) -> impl Draw<Tui> {
|
||
let note_hi = state.get_note_hi();
|
||
let note_lo = state.get_note_lo();
|
||
let note_pos = state.get_note_pos();
|
||
let note_len = state.get_note_len();
|
||
let time_pos = state.get_time_pos();
|
||
let time_start = state.get_time_start();
|
||
let time_zoom = state.get_time_zoom();
|
||
let style = Some(Style::default().fg(state.color.lightest.term));
|
||
draw(move|to: &mut Tui|{
|
||
let xywh = to.area().into();
|
||
let XYWH(x0, y0, w, _h) = xywh;
|
||
for (_area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) {
|
||
if note == note_pos {
|
||
for x in 0..w {
|
||
let screen_x = x0 + x;
|
||
let time_1 = time_start + x as usize * time_zoom;
|
||
let time_2 = time_1 + time_zoom;
|
||
if time_1 <= time_pos && time_pos < time_2 {
|
||
to.blit(&"█", screen_x, screen_y, style);
|
||
let tail = note_len as u16 / time_zoom as u16;
|
||
for x_tail in (screen_x + 1)..(screen_x + tail) {
|
||
to.blit(&"▂", x_tail, screen_y, style);
|
||
}
|
||
break
|
||
}
|
||
}
|
||
break
|
||
}
|
||
}
|
||
Ok(Some(xywh))
|
||
})
|
||
}
|
||
|
||
pub fn view_piano_h_keys (state: &PianoHorizontal) -> impl Draw<Tui> {
|
||
let color = state.color;
|
||
let note_lo = state.get_note_lo();
|
||
let note_hi = state.get_note_hi();
|
||
let note_pos = state.get_note_pos();
|
||
let key_style = Some(Style::default().fg(Rgb(192, 192, 192)).bg(Rgb(0, 0, 0)));
|
||
let off_style = Some(Style::default().fg(g(255)));
|
||
let on_style = Some(Style::default().fg(Rgb(255,0,0)).bg(color.base.term).bold());
|
||
draw(move|to: &mut Tui|{
|
||
let xywh = to.area().into();
|
||
let XYWH(x, y0, _w, _h) = xywh;
|
||
for (_area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) {
|
||
to.blit(&to_key(note), x, screen_y, key_style);
|
||
if note > 127 {
|
||
continue
|
||
}
|
||
if note == note_pos {
|
||
to.blit(&format!("{:<5}", note_pitch_to_name(note)), x, screen_y, on_style)
|
||
} else {
|
||
to.blit(¬e_pitch_to_name(note), x, screen_y, off_style)
|
||
};
|
||
}
|
||
Ok(Some(xywh))
|
||
}).exact_w(state.keys_width).full_h()
|
||
}
|
||
|
||
pub fn view_piano_h_timeline (state: &PianoHorizontal) -> impl Draw<Tui> + '_ {
|
||
draw(move|to: &mut Tui|{
|
||
let xywh = to.area().into();
|
||
let XYWH(x, y, w, _h) = xywh;
|
||
let style = Some(Style::default().dim());
|
||
let length = state.clip.read().as_ref().map(|p|p.read().length).unwrap_or(1);
|
||
for (area_x, screen_x) in (0..w).map(|d|(d, d+x)) {
|
||
let t = area_x as usize * state.time_zoom().load(Relaxed);
|
||
if t < length {
|
||
to.blit(&"|", screen_x, y, style);
|
||
}
|
||
}
|
||
Ok(Some(xywh))
|
||
}).exact_h(1).full_w()
|
||
}
|
||
|
||
impl Track {
|
||
pub fn per <'a, T: Draw<Tui> + 'a, U: TracksSizes<'a>> (
|
||
tracks: impl Fn() -> U + Send + Sync + 'a,
|
||
callback: &'a (impl Fn(usize, &'a Track)->T + Send + Sync + 'a)
|
||
) -> impl Draw<Tui> {
|
||
iter_east(move||tracks().map(|(index, track, x1, x2): (usize, &Track, usize, usize)|{
|
||
fg_bg(
|
||
track.color.lightest.term,
|
||
track.color.base.term,
|
||
callback(index, track)
|
||
).exact_w((x2 - x1) as u16)
|
||
}))
|
||
}
|
||
}
|
||
|
||
impl OctaveVertical {
|
||
pub fn tui (&self) -> impl Draw<Tui> {
|
||
east!(
|
||
fg_bg(self.color(0), self.color(1), "▙"),
|
||
fg_bg(self.color(2), self.color(3), "▙"),
|
||
fg_bg(self.color(4), self.color(5), "▌"),
|
||
fg_bg(self.color(6), self.color(7), "▟"),
|
||
fg_bg(self.color(8), self.color(9), "▟"),
|
||
fg_bg(self.color(10), self.color(11), "▟"),
|
||
)
|
||
}
|
||
}
|
||
|
||
impl Draw<Tui> for MidiEditor {
|
||
fn draw (&self, to: &mut Tui) -> Drawn<u16> {
|
||
self.autoscroll();
|
||
/*self.autozoom();*/
|
||
self.size.of(&self.mode).draw(to)
|
||
}
|
||
}
|
||
|
||
impl<T> TracksView for T where T:
|
||
HasTracks + ScenesView + HasMidiIns + HasMidiOuts + HasTrack
|
||
{}
|
||
|
||
fn iter_tracks_with_sizes <'t, T> (state: &'t T) -> impl TracksSizes<'t> where
|
||
T: HasTracks + HasEditor + HasSelection + HasClipsSize + ?Sized
|
||
{
|
||
let _editor_width = state.editor().as_ref().map(|e|e.size.w());
|
||
let _active_track = state.selection().track();
|
||
let mut x = 0;
|
||
let w = state.clips_size().w() as usize;
|
||
state.tracks().iter().enumerate().map_while(move |(index, track)|{
|
||
let width = track.width.max(8);
|
||
if x + width < w {
|
||
let data = (index, track, x, x + width);
|
||
x += width;
|
||
Some(data)
|
||
} else {
|
||
None
|
||
}
|
||
})
|
||
}
|
||
|
||
pub trait TracksView:
|
||
HasTracks + ScenesView + HasMidiIns + HasMidiOuts + HasTrack
|
||
{
|
||
/// Iterate over tracks with their corresponding sizes.
|
||
fn tracks_with_sizes (&self) -> impl TracksSizes<'_> {
|
||
iter_tracks_with_sizes(self)
|
||
}
|
||
|
||
fn view_track_column <'t, S, D, F> (&'t self, f: F) -> impl Draw<S> where
|
||
S: Screen,
|
||
D: Draw<S> + 't,
|
||
F: Fn(usize, &'t Track, &Selection, Option<&MidiEditor>)->D + Copy + 't
|
||
{
|
||
let select = self.selection();
|
||
let editor = self.editor().as_ref();
|
||
iter_east(move||self.tracks_with_sizes()
|
||
.map(move|(i, t, _x1, _x2)|f(i, t, select, editor)))
|
||
}
|
||
|
||
fn view_track_names_cells (&self) -> impl Draw<Tui> {
|
||
self.view_track_column(|i, t, select, editor|{
|
||
let b = if select.track() == Some(i) { t.color.light } else { t.color.base }.term;
|
||
bg(b, south(east!(
|
||
"·t", i, " ", fg(Rgb(255, 255, 255), bold(true, &t.name))
|
||
).align_nw().full_w(), "")).exact_wh(
|
||
view_track_w(select, i, t, editor),
|
||
2
|
||
)
|
||
})
|
||
}
|
||
|
||
fn view_track_output_cells (&self) -> impl Draw<Tui> {
|
||
self.view_track_column(|i, t, select, editor|{
|
||
let f = Rgb(255, 255, 255);
|
||
let b = t.color.dark.term;
|
||
iter_south(move||t.sequencer.midi_outs.iter().map(move|port: &MidiOutput|{
|
||
fg(f, bg(b, east!("·o", i, " ", port.port_name()).full_w().align_w()).exact_h(1))
|
||
})).full_h().align_nw().exact_w(
|
||
view_track_w(select, i, t, editor)
|
||
)
|
||
})
|
||
}
|
||
|
||
fn view_track_device_cells (&self, height: u16) -> impl Draw<Tui> + '_ {
|
||
self.view_track_column(move|i, t, select, editor|bg(
|
||
t.color.dark.term,
|
||
iter_south(move||(0..height).map(|_|fg_bg(
|
||
ItemTheme::G[32].lightest.term,
|
||
ItemTheme::G[32].dark.term,
|
||
" · --".align_nw()
|
||
).exact_wh(t.width as u16, 2))).align_nw()
|
||
).exact_wh(
|
||
view_track_w(select, i, t, editor),
|
||
height + 1
|
||
))
|
||
}
|
||
|
||
fn view_track_input_cells (&self, height: u16) -> impl Draw<Tui> {
|
||
self.view_track_column(move|i, t, select, editor|south(
|
||
bg(t.color.base.term,
|
||
east!(
|
||
either(t.sequencer.monitoring, fg(Green, "●mon "), "·mon "),
|
||
either(t.sequencer.recording, fg(Red, "●rec "), "·rec "),
|
||
either(t.sequencer.overdub, fg(Yellow, "●dub "), "·dub "),
|
||
).align_w().full_w()),
|
||
iter_south(move||t.sequencer.midi_ins.iter().map(move|port|fg_bg(
|
||
Rgb(255, 255, 255), t.color.dark.term, east!(
|
||
"·i", i, " ", port.port_name()
|
||
).align_w().full_w()
|
||
)))).align_nw().exact_wh(view_track_w(select, i, t, editor), height + 1)
|
||
).align_w()
|
||
}
|
||
|
||
fn view_input_cells (&self) -> impl Draw<Tui> {
|
||
draw(move|to: &mut Tui|{
|
||
for (_index, track, x1, _x2) in self.tracks_with_sizes() {
|
||
let _ = south(
|
||
bg(track.color.dark.term, east!(
|
||
either(track.sequencer.monitoring, fg(Green, "mon "), "mon "),
|
||
either(track.sequencer.recording, fg(Red, "rec "), "rec "),
|
||
either(track.sequencer.overdub, fg(Yellow, "dub "), "dub "),
|
||
).exact_w(track.width as u16)).align_w().push_x(x1 as u16),
|
||
draw(move |to: &mut Tui|{
|
||
for (index, port) in self.midi_ins().as_slice().iter().enumerate() {
|
||
let _ = east(
|
||
east(
|
||
" ● ",
|
||
bold(true, fg(Rgb(255,255,255), port.port_name()))
|
||
).align_w().exact_w(20),
|
||
west(
|
||
().exact_w(4),
|
||
bg(track.color.darker.term, east!(
|
||
either(track.sequencer.monitoring, fg(Green, " ● "), " · "),
|
||
either(track.sequencer.recording, fg(Red, " ● "), " · "),
|
||
either(track.sequencer.overdub, fg(Yellow, " ● "), " · "),
|
||
).exact_w(track.width as u16).align_w())
|
||
)
|
||
).push_x(index as u16 * 10).exact_h(1).draw(to)?;
|
||
}
|
||
Ok(None)
|
||
})
|
||
).draw(to)?;
|
||
}
|
||
Ok(Some(to.area()))
|
||
})
|
||
}
|
||
|
||
fn tracks_devices_height (&self) -> u16 { self.tracks().iter().fold(2, |h, t|h.max(t.devices.len() * 2)) as u16 }
|
||
fn tracks_inputs_height (&self) -> u16 { self.tracks().iter().fold(0, |h, t|h.max(t.sequencer.midi_ins.len())) as u16 }
|
||
fn outputs_height (&self) -> u16 { self.midi_outs().iter().fold(1, |h, o|h + o.connections.len()) as u16 }
|
||
|
||
fn view_track_add (&self) -> impl Draw<Tui> { button_2("T", "+", false) }
|
||
fn view_scene_add (&self) -> impl Draw<Tui> { button_2("S", "+", false) }
|
||
fn view_track_output_add (&self) -> impl Draw<Tui> { button_2("O", "+", false) }
|
||
fn view_track_input_add (&self) -> impl Draw<Tui> { button_2("I", "+", false) }
|
||
fn view_track_device_add (&self) -> impl Draw<Tui> { button_2("D", "+", false) }
|
||
fn view_track_names_add (&self) -> impl Draw<Tui> { south(self.view_track_add(), self.view_scene_add()) }
|
||
|
||
fn view_track_count (&self) -> impl Draw<Tui> { view_n_of_m("t", "rack", self.selection().track(), self.tracks().len()); }
|
||
fn view_scene_count (&self) -> impl Draw<Tui> { view_n_of_m("s", "cene", self.selection().scene(), self.scenes().len()); }
|
||
fn view_track_input_count (&self) -> impl Draw<Tui> { button_2("i", "nput", false) }
|
||
fn view_track_output_count (&self) -> impl Draw<Tui> { button_3("o", "utput", 0, false) }
|
||
fn view_track_names_count (&self) -> impl Draw<Tui> { south(self.view_track_count(), self.view_scene_count()) }
|
||
fn view_track_device_count (&self) -> impl Draw<Tui> { button_3("d", "evice", self.track().map(|t|t.devices.len()).unwrap_or(0), false).min_h(2) }
|
||
|
||
/// Draw outputs per track
|
||
fn view_track_outputs <'a> (&'a self, theme: ItemTheme, _h: u16) -> impl Draw<Tui> {
|
||
view_track_row_section(theme, self.view_track_output_count(), self.view_track_output_add(),
|
||
self.view_track_output_cells())
|
||
}
|
||
/// Draw inputs per track
|
||
fn view_track_inputs <'a> (&'a self, theme: ItemTheme) -> impl Draw<Tui> {
|
||
view_track_row_section(theme, self.view_track_input_count(), self.view_track_input_add(),
|
||
self.view_track_input_cells(self.tracks_inputs_height()))
|
||
}
|
||
/// Draw devices of track
|
||
fn view_track_devices (&self, theme: ItemTheme) -> impl Draw<Tui> {
|
||
view_track_row_section(theme, self.view_track_device_count(), self.view_track_device_add(),
|
||
self.view_track_device_cells(self.tracks_devices_height()))
|
||
}
|
||
/// Draw name of each track
|
||
fn view_track_names (&self, theme: ItemTheme) -> impl Draw<Tui> {
|
||
view_track_row_section(theme, self.view_track_names_count(), self.view_track_names_add(),
|
||
self.view_track_names_cells())
|
||
}
|
||
|
||
fn view_inputs (&self) -> impl Draw<Tui> + '_ {
|
||
let title_1 = button_3("i", "nput ", self.midi_ins().len(), false).align_w().exact_wh(20, 1);
|
||
let title_2 = self.view_track_input_add().exact_wh(4, 1);
|
||
east(title_1, west(title_2, self.view_input_cells()))
|
||
}
|
||
|
||
fn view_outputs (&self, theme: ItemTheme) -> impl Draw<Tui> {
|
||
let height = self.outputs_height();
|
||
let list = self.view_outputs_list(height);
|
||
view_track_row_section(theme, list, self.view_track_output_add(),
|
||
bg(theme.darker.term, self.view_outputs_grid().align_w().full_w())).exact_h(height)
|
||
}
|
||
|
||
fn view_outputs_list (&self, height: u16) -> impl Draw<Tui> {
|
||
south(
|
||
button_3("o", "utput", self.midi_outs().len(), false).align_w().full_w().exact_h(1),
|
||
bg(Color::Rgb(0, 0, 0), draw(|to: &mut Tui|{
|
||
for (_index, port) in self.midi_outs().iter().enumerate() {
|
||
east(
|
||
east(" ● ", fg(Rgb(255,255,255), bold(true, port.port_name())))
|
||
.align_w(),
|
||
east!(port.port().get_connections().len(), "/", port.connections.len(), " ")
|
||
.align_e().full_w().exact_h(1)).full_w().draw(to)?;
|
||
for (index, conn) in port.connections.iter().enumerate() {
|
||
east!(" c", index, conn.info()).align_w().full_w().exact_h(1).draw(to)?;
|
||
}
|
||
}
|
||
todo!();
|
||
})).align_nw().full_wh().exact_h(height.minus(1)).min_h(1)
|
||
)
|
||
}
|
||
|
||
fn view_outputs_grid (&self) -> impl Draw<Tui> {
|
||
draw(|to: &mut Tui|{
|
||
for (index, track, _x1, _x2) in self.tracks_with_sizes() {
|
||
let _ = draw(|to: &mut Tui|{
|
||
east(either(true, fg(Green, "play "), "play "),
|
||
either(false, fg(Yellow, "solo "), "solo ")).align_w().exact_h(1).draw(to)?;
|
||
for (_index, port) in self.midi_outs().iter().enumerate() {
|
||
east(either(true, fg(Green, " ● "), " · "),
|
||
either(false, fg(Yellow, " ● "), " · ")).align_w().exact_h(1).draw(to)?;
|
||
for (_index, _conn) in port.connections.iter().enumerate() {
|
||
"".full_w().exact_h(1).draw(to)?;
|
||
}
|
||
}
|
||
todo!()
|
||
}).exact_w(track_width(index, track)).draw(to)?;
|
||
}
|
||
todo!()
|
||
})
|
||
}
|
||
|
||
}
|
||
|
||
fn view_n_of_m (key: &str, name: &str, index: Option<usize>, count: usize) -> impl Draw<Tui> {
|
||
button_3(key, name, east(index, count), false)
|
||
}
|
||
|
||
pub trait ScenesView: HasEditor + HasSelection + HasSceneScroll + HasClipsSize + Send + Sync {
|
||
fn h_scenes (&self) -> u16;
|
||
fn w_side (&self) -> u16;
|
||
fn w_mid (&self) -> u16;
|
||
fn scenes_with_sizes (&self) -> impl ScenesSizes<'_> {
|
||
iter_scenes_with_sizes(self)
|
||
}
|
||
fn view_scenes_names (&self) -> impl Draw<Tui> where Self: Sized {
|
||
let select = self.selection();
|
||
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.as_ref(), index, scene, editing)
|
||
}))
|
||
}
|
||
}
|
||
|
||
fn iter_scenes_with_sizes <'s, T> (state: &'s T) -> impl ScenesSizes<'s> where T:
|
||
HasScenes + HasSceneScroll + HasEditor + HasSelection + HasClipsSize + ?Sized
|
||
{
|
||
let mut y = 0;
|
||
let skip = state.scene_scroll();
|
||
let selected = state.selection();
|
||
let editor = state.editor();
|
||
let clips_size = state.clips_size();
|
||
state.scenes().iter().enumerate().skip(skip).map_while(move|(s, scene)|{
|
||
let height = if selected.scene() == Some(s)
|
||
&& let Some(editor) = editor
|
||
{
|
||
editor.size.h() as usize
|
||
} else {
|
||
Scene::DEFAULT_HEIGHT
|
||
};
|
||
|
||
if y + height <= clips_size.h() as usize {
|
||
let data = (s, scene, y, y + height);
|
||
y += height;
|
||
Some(data)
|
||
} else {
|
||
None
|
||
}
|
||
})
|
||
}
|
||
|
||
fn view_modes <'a, S: HasModeStack> (state: &'a S) -> impl Draw<Tui> + 'a {
|
||
draw(move|to: &mut Tui|{
|
||
let XYWH(x, y, w, h) = to.area();
|
||
let mut x_next = x;
|
||
let mut w_used = 0u16;
|
||
for item in state.modes().iter() {
|
||
let area = XYWH(x_next, y, w.minus(w_used), h);
|
||
if let Some(used) = to.draw(area, item)? {
|
||
let used_w = used.w() + 1;
|
||
x_next = x_next.plus(used_w);
|
||
w_used = w_used.plus(used_w);
|
||
if w_used >= w {
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
Ok(Some(XYWH(x, y, w_used, h)))
|
||
})
|
||
}
|
||
|
||
pub fn view_track_w (
|
||
select: &Selection, track_index: usize, track: &Track, editor: Option<&MidiEditor>
|
||
) -> u16 {
|
||
if select.track() == Some(track_index) && let Some(editor) = editor {
|
||
(editor.size.w() as usize).max(20).max(track.width) as u16
|
||
} else {
|
||
track.width as u16
|
||
}
|
||
}
|
||
|
||
pub(crate) fn track_width (_index: usize, track: &Track) -> u16 {
|
||
track.width as u16
|
||
}
|
||
|
||
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> {
|
||
Split::West.bar(4u16, button_add.align_ne().full_wh(),
|
||
Split::East.bar(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 {
|
||
editor.size.h() as u16
|
||
} else {
|
||
Scene::DEFAULT_HEIGHT as u16
|
||
};
|
||
let a = east!("·s", index, " ", fg(g(255), bold(true, &scene.name))).align_nw().full_w();
|
||
let s = select.scene() == Some(index) && editing;
|
||
let b = when(s, south(editor.map(view_clip_info), editor.map(view_edit_info)).full_wh().align_se());
|
||
let c = if select.scene() == Some(index) {
|
||
scene.color.light.term
|
||
} else {
|
||
scene.color.base.term
|
||
};
|
||
bg(c, above(a, b.push_y(1)).align_nw().exact_h(h))
|
||
}
|
||
|
||
/// Draw clips per scene
|
||
fn view_scenes_clips <S: TracksView + ScenesView> (state: &S) -> impl Draw<Tui> {
|
||
let select = state.selection();
|
||
let editor = state.editor();
|
||
let editing = state.is_editing();
|
||
let sized = move|it|state.with_clips_size(true, it).align_c();
|
||
let tracks = move|it|iter_east(move||state.tracks_with_sizes().map(it));
|
||
let scenes = move|it|iter_south(move||state.scenes_with_sizes().map(it));
|
||
sized(tracks(move|(track_index, track, _, _)|{
|
||
let w = view_track_w(select, track_index, track, editor.as_ref()) as u16;
|
||
scenes(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_sel = view_scene_sel(select, track_index, scene_index, editing);
|
||
let border = Outer(true, Style::default().fg(o)).full_wh();
|
||
let title = below(fg_bg(o, b, "".full_wh()),
|
||
fg_bg(f, b, bold(true, name)).align_nw().full_wh());
|
||
above(border, above(title, when(is_sel, editor).full_wh()).full_wh())
|
||
.exact_wh(w, view_scene_y(select, scene_index, editor.as_ref()),)
|
||
}).exact_w(w)
|
||
}))
|
||
}
|
||
|
||
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_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_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(13)
|
||
} else {
|
||
Scene::DEFAULT_HEIGHT as u16
|
||
}
|
||
}
|
||
|
||
pub fn view_clip_info (editor: &MidiEditor) -> impl Draw<Tui> + '_ {
|
||
let (
|
||
_color, name, length, looped
|
||
) = if let Some(clip) = editor.mode.clip.read().as_ref().map(|p|p.read()) {
|
||
(clip.color, clip.name.clone(), clip.length, clip.looped)
|
||
} else {
|
||
(ItemTheme::G[64], String::new().into(), 0, false)
|
||
};
|
||
south!(
|
||
south(
|
||
button_2("f", "name ", false),
|
||
fg_bg(Rgb(255, 255, 255), Rgb(16, 16, 16), format!("{name} ").origin_e().pad_w(1).full_w())
|
||
).origin_w().full_w(),
|
||
east(
|
||
button_2("l", "ength ", false),
|
||
fg(Rgb(255, 255, 255), format!("{length} ")).origin_e().full_w()
|
||
).origin_w().full_w(),
|
||
east(
|
||
button_2("r", "epeat ", false),
|
||
fg(Rgb(255, 255, 255), format!("{looped} ")).origin_e().full_w()
|
||
).origin_w().full_w(),
|
||
)
|
||
}
|
||
|
||
pub fn view_edit_info (editor: &MidiEditor) -> impl Draw<Tui> + '_ {
|
||
let (_color, length) = if let Some(clip) = editor.mode.clip.read().as_ref().map(|p|p.read()) {
|
||
(clip.color, clip.length)
|
||
} else { (ItemTheme::G[64], 0) };
|
||
let time_pos = editor.get_time_pos();
|
||
let time_zoom = editor.get_time_zoom();
|
||
let time_lock = if editor.get_time_lock() { "[lock]" } else { " " };
|
||
let note_pos = editor.get_note_pos();
|
||
let note_name = format!("{:4}", note_pitch_to_name(note_pos));
|
||
let note_pos = format!("{:>3}", note_pos);
|
||
let note_len = format!("{:>4}", editor.get_note_len());
|
||
south!(
|
||
east(
|
||
button_2("t", "ime ", false),
|
||
fg(Rgb(255, 255, 255), format!("{length} /{time_zoom} +{time_pos} ")).origin_e().pad_w(1).full_w()
|
||
).origin_w().full_w(),
|
||
east(
|
||
button_2("z", "lock ", false),
|
||
fg(Rgb(255, 255, 255), format!("{time_lock}")).origin_e().full_w()
|
||
).origin_w().full_w(),
|
||
south(
|
||
button_2("x", "note ", false),
|
||
fg_bg(Rgb(255, 255, 255), Rgb(16, 16, 16), format!("{note_name} {note_pos} {note_len}").origin_e().full_w())
|
||
).origin_w().full_w(),
|
||
)
|
||
}
|
||
|
||
pub fn view_scene_name_theme (scene: &Scene, track_index: usize) -> (Arc<str>, ItemTheme) {
|
||
scene.clips
|
||
.get(track_index)
|
||
.as_ref()
|
||
.map(|clip|clip.as_ref().map(|clip|{
|
||
let clip = clip.read();
|
||
(format!(" ⏹ {}", &clip.name).into(), clip.color)
|
||
}))
|
||
.flatten()
|
||
.unwrap_or_else(||{
|
||
(" ⏹ -- ".into(), ItemTheme::G[32])
|
||
})
|
||
}
|
||
|
||
pub fn view_selection <'a> (state: &'a App) -> impl Draw<Tui> + use<'a> {
|
||
format!("{:?}", state.selection())
|
||
}
|
||
|
||
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)))
|
||
}
|
||
|
||
pub fn redraw_piano (state: &PianoHorizontal) -> BigBuffer {
|
||
state.clip.map_inner(|clip|{
|
||
let mut buffer = BigBuffer::from(state.buffer_size(&clip));
|
||
let time_zoom = state.get_time_zoom().max(1);
|
||
state.time_len().store(clip.length, Relaxed);
|
||
let note_len = state.get_note_len();
|
||
let note_pos = state.get_note_pos();
|
||
let time_pos = state.get_time_pos();
|
||
draw_piano_bg(&mut buffer, &clip, time_zoom, note_len, note_pos, time_pos);
|
||
draw_piano_fg(&mut buffer, &clip, time_zoom);
|
||
buffer
|
||
}).unwrap_or_default()
|
||
}
|
||
|
||
/// Draw the piano roll background.
|
||
pub fn draw_piano_bg (
|
||
buf: &mut BigBuffer,
|
||
clip: &MidiClip,
|
||
zoom: usize,
|
||
note_len: usize,
|
||
note_point: usize,
|
||
time_point: usize
|
||
) {
|
||
for (y, note) in (0..=127).rev().enumerate() {
|
||
for (x, time) in (0..buf.width).map(|x|(x, x*zoom)) {
|
||
let cell = buf.get_mut(x, y).unwrap();
|
||
if note == (127-note_point) || time == time_point {
|
||
cell.set_bg(Rgb(0,0,0));
|
||
} else {
|
||
cell.set_bg(clip.color.darkest.term);
|
||
}
|
||
if time % 384 == 0 {
|
||
cell.set_fg(clip.color.darker.term);
|
||
cell.set_char('│');
|
||
} else if time % 96 == 0 {
|
||
cell.set_fg(clip.color.dark.term);
|
||
cell.set_char('╎');
|
||
} else if time % note_len == 0 {
|
||
cell.set_fg(clip.color.darker.term);
|
||
cell.set_char('┊');
|
||
} else if (127 - note) % 12 == 0 {
|
||
cell.set_fg(clip.color.darker.term);
|
||
cell.set_char('=');
|
||
} else if (127 - note) % 6 == 0 {
|
||
cell.set_fg(clip.color.darker.term);
|
||
cell.set_char('-');
|
||
} else {
|
||
cell.set_fg(clip.color.darker.term);
|
||
cell.set_char('·');
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Draw the piano roll foreground.
|
||
pub fn draw_piano_fg (
|
||
buf: &mut BigBuffer,
|
||
clip: &MidiClip,
|
||
zoom: usize
|
||
) {
|
||
let style = Style::default().fg(clip.color.base.term);//.bg(Rgb(0, 0, 0));
|
||
let mut notes_on = [false;128];
|
||
for (x, time_start) in (0..clip.length).step_by(zoom).enumerate() {
|
||
for (_y, note) in (0..=127).rev().enumerate() {
|
||
if let Some(cell) = buf.get_mut(x, note) {
|
||
if notes_on[note] {
|
||
cell.set_char('▂');
|
||
cell.set_style(style);
|
||
}
|
||
}
|
||
}
|
||
let time_end = time_start + zoom;
|
||
for time in time_start..time_end.min(clip.length) {
|
||
if let Some(notes) = clip.notes.get(time) {
|
||
for event in notes.iter() {
|
||
match event {
|
||
MidiMessage::NoteOn { key, .. } => {
|
||
let note = key.as_int() as usize;
|
||
if let Some(cell) = buf.get_mut(x, note) {
|
||
cell.set_char('█');
|
||
cell.set_style(style);
|
||
}
|
||
notes_on[note] = true
|
||
},
|
||
MidiMessage::NoteOff { key, .. } => {
|
||
notes_on[key.as_int() as usize] = false
|
||
},
|
||
_ => {}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
#[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
|
||
}
|
||
}
|
||
|
||
/// 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));
|
||
}
|
||
}
|
||
}
|
||
|
||
#[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]);
|
||
}
|
||
}
|
||
}
|
||
|
||
//impl<'a> Iterator for EntriesIterator<'a, Tui> {
|
||
//type Item = impl Draw<Tui>;
|
||
//fn next (&mut self) -> Option<Self::Item> {
|
||
//let dirs = self.browser.dirs.len();
|
||
//let files = self.browser.files.len();
|
||
//let index = self.index;
|
||
//if self.index < dirs {
|
||
//self.index += 1;
|
||
//Some(bold(true, self.browser.dirs[index].1.as_str()))
|
||
//} else if self.index < dirs + files {
|
||
//self.index += 1;
|
||
//Some(bold(false, self.browser.files[index - dirs].1.as_str()))
|
||
//} else {
|
||
//None
|
||
//}
|
||
//}
|
||
//}
|
||
|
||
//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),
|
||
//}
|
||
//}
|
||
//}
|