mirror of
https://codeberg.org/unspeaker/tek.git
synced 2026-10-08 18:56:42 +02:00
321 lines
17 KiB
Rust
321 lines
17 KiB
Rust
use crate::*;
|
|
|
|
/// Collection of custom view definitions.
|
|
pub type Views = Arc<RwLock<BTreeMap<Arc<str>, Arc<View<Tui, App>>>>>;
|
|
|
|
/// 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(format!("not word/expr:\n{source:?}").into())
|
|
};
|
|
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(format!("invalid split: {split:?}").into())
|
|
};
|
|
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(format!("invalid split: {split:?}").into())
|
|
};
|
|
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(format!("invalid azimuth: {azimuth:?}").into())
|
|
};
|
|
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" => {
|
|
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 {
|
|
"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 {
|
|
"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 {
|
|
"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(format!("compile_expr: unexpected: {expr:?}").into())
|
|
|
|
}
|
|
|
|
} else {
|
|
return Err(format!("compile_expr: invalid expression: {expr:?}").into())
|
|
}))
|
|
}
|
|
|
|
fn compile_word (word: Arc<str>)
|
|
-> Usually<Arc<Box<dyn Fn(&App, &mut Tui)->Drawn<u16> + Send + Sync>>>
|
|
{
|
|
macro_rules! draw {
|
|
(|$state:ident|$body:expr)=>{Self::boxed(move|$state, to|$body.draw(to))}
|
|
}
|
|
|
|
Ok(Arc::new(match word.split("/").next() {
|
|
//Some(":logo") => view_logo().draw(to),
|
|
Some(":meters") => match word.split("/").skip(1).next() {
|
|
Some("input") => Self::boxed(move|_, to|bg(Rgb(30, 30, 30), "Input Meters".align_s().full_h()).draw(to)),
|
|
Some("output") => Self::boxed(move|_, to|bg(Rgb(30, 30, 30), "Output Meters".align_s().full_h()).draw(to)),
|
|
_ => panic!()
|
|
},
|
|
Some(":tracks") => match &word[(":tracks".len()+1)..] {
|
|
"" => Self::boxed(move|_, to|"TODO tracks".draw(to)),
|
|
|
|
"names" => draw!(|app|app.project.view_track_names(app.color.clone())),
|
|
"names/count" => draw!(|app|app.project.view_track_names_count()),
|
|
"names/cells" => draw!(|app|app.project.view_track_names_cells()),
|
|
"names/add" => draw!(|app|app.project.view_track_names_add()),
|
|
|
|
"devices" => draw!(|app|app.project.view_track_devices(app.color.clone())),
|
|
"devices/count" => draw!(|app|app.project.view_track_device_count()),
|
|
"devices/cells" => draw!(|app|app.project.view_track_device_cells(app.tracks_devices_height())),
|
|
"devices/add" => draw!(|app|app.project.view_track_device_add()),
|
|
|
|
"inputs" => draw!(|app|app.project.view_track_inputs(app.color.clone())),
|
|
"inputs/count" => draw!(|app|app.project.view_track_input_count()),
|
|
"inputs/cells" => draw!(|app|app.project.view_track_input_cells(app.tracks_inputs_height())),
|
|
"inputs/add" => draw!(|app|app.project.view_track_input_add()),
|
|
|
|
"outputs" => draw!(|app|app.project.view_track_outputs(app.color.clone(), 0)),
|
|
"outputs/count" => draw!(|app|app.project.view_track_output_count()),
|
|
"outputs/cells" => draw!(|app|app.project.view_track_output_cells()),
|
|
"outputs/add" => draw!(|app|app.project.view_track_output_add()),
|
|
|
|
_ => panic!("unknown: {word}")
|
|
},
|
|
Some(":scenes") => match word.split("/").skip(1).next() {
|
|
Some("clips") => draw!(|app|app.view_scenes_clips()),
|
|
Some("names") => draw!(|app|app.view_scenes_names()),
|
|
_ => panic!()
|
|
},
|
|
Some(":templates") => draw!(|app|view_templates(app)),
|
|
Some(":browse/title") => draw!(|app|view_browse_title(app)),
|
|
Some(":device") => draw!(|app|view_device(app)),
|
|
Some(":dialog") => Self::boxed(move|state, to|draw_dialog(to, word.split("/").skip(1), state)),
|
|
Some(":sessions") => Self::boxed(move|_, to|view_sessions().draw(to)),
|
|
Some(":status") => Self::boxed(move|state, to|draw_status_bar(to, state)),
|
|
Some(":editor") => Self::boxed(move|_, to|"TODO Editor".draw(to)),
|
|
Some(":transport") => Self::boxed(move|_, to|view_transport(true, "", "", "").draw(to)),
|
|
Some(":debug") => Self::boxed(move|_, to|format!("[{:?}]", to.area()).exact_h(1).draw(to)),
|
|
Some(_) => Self::boxed(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!()
|
|
}))
|
|
}
|
|
}
|
|
|
|
/// 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(())
|
|
}
|