collect tek_view, tek_ctrl

This commit is contained in:
i do not exist 2026-09-21 00:16:58 +03:00
parent c5d6ce24b2
commit 186b81f29c
47 changed files with 2665 additions and 2893 deletions

553
src/tek_view.rs Normal file
View file

@ -0,0 +1,553 @@
use crate::*;
pub fn draw_sample (
to: &mut Tui, x: u16, y: u16, note: Option<&u7>, sample: &Sample, focus: bool
) -> Usually<usize> {
let style = if focus { Style::default().green() } else { Style::default() };
if focus {
to.blit(&"🬴", x+1, y, Some(style.bold()));
}
let label1 = format!("{:3} {:12}",
note.map(|n|n.to_string()).unwrap_or(String::default()),
sample.name);
let label2 = format!("{:>6} {:>6} +0.0",
sample.start,
sample.end);
to.blit(&label1, x+2, y, Some(style.bold()));
to.blit(&label2, x+3+label1.len()as u16, y, Some(style));
Ok(label1.len() + label2.len() + 4)
}
pub fn draw_sample_list_item (sample: &Option<Arc<RwLock<Sample>>>) -> String {
if let Some(sample) = sample {
let sample = sample.try_read().unwrap();
format!("{:8}", sample.name)
//format!("{:8} {:3} {:6}-{:6}/{:6}",
//sample.name,
//sample.gain,
//sample.start,
//sample.end,
//sample.channels[0].len()
//)
} else {
String::from("........")
}
}
pub fn draw_sample_viewer (sample: Option<&Arc<RwLock<Sample>>>) -> impl Draw<Tui> + use<'_> {
let min_db = -64.0;
draw(move|to: &mut Tui|{
let xywh = to.area().into();
let XYWH(x, y, width, height) = xywh;
let area = Rect { x, y, width, height };
if let Some(sample) = &sample {
let sample = sample.try_read().unwrap();
let start = sample.start as f64;
let end = sample.end as f64;
let length = end - start;
let step = length / width as f64;
let mut t = start;
let mut lines = vec![];
while t < end {
let chunk = &sample.channels[0][t as usize..((t + step) as usize).min(sample.end)];
let total: f32 = chunk.iter().map(|x|x.abs()).sum();
let count = chunk.len() as f32;
let meter = 10. * (total / count).log10();
let x = t as f64;
let y = meter as f64;
lines.push(Line::new(x, min_db, x, y, Color::Green));
t += step / 2.;
}
to.buffer().map(|buf|{
Canvas::default()
.x_bounds([sample.start as f64, sample.end as f64])
.y_bounds([min_db, 0.])
.paint(|ctx| {
for line in lines.iter() {
ctx.draw(line);
}
//FIXME: proportions
//let text = "press record to finish sampling";
//ctx.print(
//(width - text.len() as u16) as f64 / 2.0,
//height as f64 / 2.0,
//text.red()
//);
})
.render(area, buf);
});
} else {
to.buffer().map(|buf|{
Canvas::default()
.x_bounds([0.0, width as f64])
.y_bounds([0.0, height as f64])
.paint(|_ctx| {
//let text = "press record to begin sampling";
//ctx.print(
//(width - text.len() as u16) as f64 / 2.0,
//height as f64 / 2.0,
//text.red()
//);
})
.render(area, buf);
});
}
Ok(Some(xywh))
})
}
pub fn draw_meters (meters: &[f32]) -> impl Draw<Tui> + use<'_> {
bg(Black, iter_east_fixed(1, ||meters.iter(), |value, _index|{
RmsMeter(*value).full_h()
}).exact_w(2))
}
#[cfg(feature = "lv2")]
pub fn draw_lv2_header (state: &Lv2, to: &mut Tui, x: u16, y: u16, w: u16) {
let style = Style::default().gray();
let label1 = format!(" {}", state.name);
to.blit(&label1, x + 1, y, Some(style.white().bold()));
if let Some(ref path) = state.path {
let label2 = format!("{}…", &path[..((w as usize - 10).min(path.len()))]);
to.blit(&label2, x + 2 + label1.len() as u16, y, Some(style.not_dim()));
}
//Ok(Rect { x, y, width: w, height: 1 })
}
pub fn draw_status_bar (to: &mut Tui, state: &App) -> Drawn<u16> {
bar(Split::South, 1u16,
dim(true, "Mode"),
bold(true, state.modes.read().get(0))
).push_x(1).draw(to)
}
/// ```
/// let x = "";
/// let _ = tek::view_status(None, x.as_ref(), x.as_ref(), x.as_ref());
/// let _ = tek::view_status(Some("".into()), x.as_ref(), x.as_ref(), x.as_ref());
/// ```
pub fn view_status <'a> (sel: Option<&str>, sr: &str, buf: &str, lat: &str) -> impl Draw<Tui> {
let theme = ItemTheme::G[96];
let sr = field_h(theme, "SR", sr);
let buf = field_h(theme, "Buf", buf);
let lat = field_h(theme, "Lat", lat);
bg(Black, east!(above(
sel.map(|sel|field_h(theme, "Selected", sel)).align_w().full_wh(),
east!(sr, buf, lat).align_e().full_wh(),
)))
}
pub fn view_device <'a> (state: &'a App) -> impl Draw<Tui> {
let selected = state.dialog.device_kind().unwrap();
south(
bold(true, "Add device"),
iter_south(move||device_kinds().iter().enumerate().map(move|(i, _label)|{
let b = if i == selected { Rgb(64,128,32) } else { Rgb(0,0,0) };
let l = if i == selected { "[ " } else { " " };
let r = if i == selected { " ]" } else { " " };
bg(b, east(l, west(r, "FIXME device name"))).full_w()
})))
}
pub fn view_track_row_section <'a> (
_theme: ItemTheme,
button: impl Draw<Tui>,
button_add: impl Draw<Tui>,
content: impl Draw<Tui>,
) -> impl Draw<Tui> {
bar(
Split::West,
4u16,
button_add.align_ne().full_wh(),
bar(
Split::East,
20u16,
button.full_wh().align_nw(),
content.align_c().full_wh()
)
)
}
pub fn view_track_header <'a> (theme: ItemTheme, content: impl Draw<Tui>)
-> impl Draw<Tui>
{
bg(theme.darker.term, content.align_e().full_w()).exact_w(12)
}
pub fn view_scene_name <'a> (
select: &Selection,
editor: Option<&'a MidiEditor>,
index: usize,
scene: &Scene,
editing: bool
) -> impl Draw<Tui> {
let h = if select.scene() == Some(index) && let Some(_editor) = editor {
7
} else {
Scene::DEFAULT_HEIGHT as u16
};
let a = east!("·s", index, " ", fg(g(255), bold(true, &scene.name))).align_w().full_w();
let b = when(select.scene() == Some(index) && editing, south(
editor.as_ref().map(|e|e.clip_status()),
editor.as_ref().map(|e|e.edit_status())).align_nw().full_wh());
let c = if select.scene() == Some(index) {
scene.color.light.term
} else {
scene.color.base.term
};
bg(c, south(a, b).align_nw().exact_h(h))
}
pub fn view_scene_name_theme (scene: &Scene, track_index: usize) -> (Arc<str>, ItemTheme) {
if let Some(Some(clip)) = &scene.clips.get(track_index) {
let clip = clip.try_read().unwrap();
(format!(" ⏹ {}", &clip.name).into(), clip.color)
} else {
(" ⏹ -- ".into(), ItemTheme::G[32])
}
}
pub fn view_scene_bg (
theme: ItemTheme, select: &Selection, track_index: usize, scene_index: usize
) -> (Color, Color) {
let mut outline = theme.base.term;
(if select.track() == Some(track_index) && select.scene() == Some(scene_index) {
outline = theme.lighter.term;
theme.light.term
} else if select.track() == Some(track_index) || select.scene() == Some(scene_index) {
outline = theme.darkest.term;
theme.base.term
} else {
theme.dark.term
}, outline)
}
pub fn view_scene_w (
track: &Track, select: &Selection, track_index: usize, editor: Option<&MidiEditor>
) -> u16 {
if select.track() == Some(track_index) && let Some(editor) = editor {
(editor.size.w() as usize).max(24).max(track.width) as u16
} else {
track.width as u16
}
}
pub fn view_scene_y (
select: &Selection, scene_index: usize, editor: Option<&MidiEditor>
) -> u16 {
if select.scene() == Some(scene_index) && let Some(editor) = editor {
editor.size.h().max(12)
} else {
Scene::DEFAULT_HEIGHT as u16
}
}
pub fn view_scene_sel (select: &Selection, track_index: usize, scene_index: usize, editing: bool) -> bool {
editing && select.track() == Some(track_index) && select.scene() == Some(scene_index)
}
pub fn view_templates <'a> (state: &'a App) -> impl Draw<Tui> + use<'a> {
let height = (state.config.modes.len() * 2) as u16;
draw(move |to: &mut Tui|{
let mut index = 0;
state.config.modes.for_each(&mut |id: &str, profile: &Mode| {
let b = if index == 0 { Rgb(70,70,70) } else { Rgb(50,50,50) };
let name = profile.name.get(0).map(|x|x.as_ref()).unwrap_or("<no name>");
let info = profile.info.get(0).map(|x|x.as_ref()).unwrap_or("<no info>");
let fg1 = Rgb(224, 192, 128);
let fg2 = Rgb(224, 128, 32);
let field_name = fg(fg1, name).align_w().full_w();
let field_id = fg(fg2, id).align_e().full_w();
let field_info = info.align_w().full_w();
let _ = bg(b, south(above(field_name, field_id), field_info))
.full_w().exact_h(2).push_y((2 * index) as u16).draw(to);
index += 1;
});
Ok(Some(to.area().into()))
}).min_w(30).exact_h(height)
}
pub fn view_browse_title (state: &App) -> impl Draw<Tui> {
field_v(ItemTheme::default(), match state.dialog.browser_target().unwrap() {
BrowseTarget::SaveProject => "Save project:",
BrowseTarget::LoadProject => "Load project:",
BrowseTarget::ImportSample(_) => "Import sample:",
BrowseTarget::ExportSample(_) => "Export sample:",
BrowseTarget::ImportClip(_) => "Import clip:",
BrowseTarget::ExportClip(_) => "Export clip:",
}, fg(g(96), x_repeat("🭻")).exact_h(1)).align_w().full_w()
}
/// ```
/// let _ = tek::view_meter("", 0.0);
/// let _ = tek::view_meters(&[0.0, 0.0]);
/// ```
pub fn view_meter <'a> (label: &'a str, value: f32)
-> impl Draw<Tui>
{
let f = field_h(ItemTheme::G[128], label, format!("{:>+9.3}", value));
let w = if value >= 0.0 { 13 }
else if value >= -1.0 { 12 }
else if value >= -2.0 { 11 }
else if value >= -3.0 { 10 }
else if value >= -4.0 { 9 }
else if value >= -6.0 { 8 }
else if value >= -9.0 { 7 }
else if value >= -12.0 { 6 }
else if value >= -15.0 { 5 }
else if value >= -20.0 { 4 }
else if value >= -25.0 { 3 }
else if value >= -30.0 { 2 }
else if value >= -40.0 { 1 }
else { 0 };
let c = if value >= 0.0 { Red }
else if value >= -3.0 { Yellow }
else { Green };
south!(f, bg(c, ()).exact_wh(w, 1))
}
pub fn view_meters (values: &[f32;2]) -> impl Draw<Tui> + use<'_> {
let left = format!("L/{:>+9.3}", values[0]);
let right = format!("R/{:>+9.3}", values[1]);
south(left, right)
}
pub fn view_midi_ins_status (theme: ItemTheme, track: Option<&Track>) -> impl Draw<Tui> {
track.map(move|track|view_ports_status(theme, "MIDI ins: ", &track.sequencer.midi_ins))
}
pub fn view_midi_outs_status (theme: ItemTheme, track: Option<&Track>) -> impl Draw<Tui> {
track.map(move|track|view_ports_status(theme, "MIDI outs: ", &track.sequencer.midi_outs))
}
pub fn view_audio_ins_status (theme: ItemTheme, track: Option<&Track>) -> impl Draw<Tui> {
track.map(move|track|view_ports_status(theme, "Audio ins: ", &track.audio_ins()))
}
pub fn view_audio_outs_status (theme: ItemTheme, track: Option<&Track>) -> impl Draw<Tui> {
track.map(move|track|view_ports_status(theme, "Audio outs:", &track.audio_outs()))
}
pub fn view_ports_status <'a, T: JackPort> (theme: ItemTheme, title: &'a str, ports: &'a [T])
-> impl Draw<Tui> + use<'a, T>
{
let ins = ports.len() as u16;
let frame = Outer(true, Style::default().fg(g(96)));
border(true, frame, field_v(theme, title, iter_south({
move||ports.iter().enumerate().map(|(index, port)|{
east!(" ", index, " ", port.port_name()).align_w().full_h()
})
})).exact_wh(20, 1 + ins)).exact_wh(20, 1 + ins)
}
pub fn view_io_ports <'a, T: PortsSizes<'a>> (
fg: Color, bg: Color, items: impl Fn()->T + Send + Sync + 'a
) -> impl Draw<Tui> + 'a {
type Item<'a> = (usize, &'a Arc<str>, &'a [Connect], usize, usize);
iter(items,
move|(_index, name, connections, y, y2): Item<'a>, _| south(
bold(true, fg_bg(fg, bg, east(" 󰣲 ", name).align_w())).full_h(),
iter(||connections.iter(), move|connect: &'a Connect, index|{
bold(false, fg_bg(fg, bg, &connect.info)).exact_h(1).align_w().push_y(index as u16)
})
).exact_h((y2 - y) as u16).push_y(y as u16))
}
pub fn view_sample_info (sample: Option<&Arc<RwLock<Sample>>>) -> impl Draw<Tui> + use<'_> {
when(sample.is_some(), draw(move|to: &mut Tui|{
let sample = sample.unwrap().try_read().unwrap();
let theme = sample.color;
east!(
field_h(theme, "Name", format!("{:<10}", sample.name.clone())),
field_h(theme, "Length", format!("{:<8}", sample.channels[0].len())),
field_h(theme, "Start", format!("{:<8}", sample.start)),
field_h(theme, "End", format!("{:<8}", sample.end)),
field_h(theme, "Trans", "0"),
field_h(theme, "Gain", format!("{}", sample.gain)),
).draw(to)
}))
}
pub fn view_sample_info_v (sample: Option<&Arc<RwLock<Sample>>>) -> impl Draw<Tui> + use<'_> {
let a = draw(move|to: &mut Tui|{
let sample = sample.unwrap().try_read().unwrap();
let theme = sample.color;
south!(
field_h(theme, "Name ", format!("{:<10}", sample.name.clone())) .align_w().full_w(),
field_h(theme, "Length", format!("{:<8}", sample.channels[0].len())).align_w().full_w(),
field_h(theme, "Start ", format!("{:<8}", sample.start)) .align_w().full_w(),
field_h(theme, "End ", format!("{:<8}", sample.end)) .align_w().full_w(),
field_h(theme, "Trans ", "0") .align_w().full_w(),
field_h(theme, "Gain ", format!("{}", sample.gain)) .align_w().full_w(),
).exact_w(20).draw(to)
});
let b = draw(|to: &mut Tui|fg(Red, south!(
bold(true, "× No sample."),
"[r] record",
"[Shift-F9] import",
)).draw(to));
either(sample.is_some(), a, b)
}
pub fn view_sample_status (sample: Option<&Arc<RwLock<Sample>>>) -> impl Draw<Tui> {
bold(true, fg(g(224), sample
.map(|sample|{
let sample = sample.try_read().unwrap();
format!("Sample {}-{}", sample.start, sample.end)
})
.unwrap_or_else(||"No sample".to_string())))
}
/// ```
/// let _ = tek::button_2("", "", true);
/// let _ = tek::button_2("", "", false);
/// ```
pub fn button_2 <'a> (key: impl Draw<Tui>, label: impl Draw<Tui>, hide: bool)
-> impl Draw<Tui>
{
let c1 = tui_orange();
let c2 = tui_g(0);
let c3 = tui_g(96);
let c4 = tui_g(255);
bold(true, fg_bg(c1, c2,
bar(Split::East, 1u16, fg(c2, "▐"),
bar(Split::East, 1u16, key,
bar(Split::East, 1u16, fg(c3, "▐"),
when(!hide, fg_bg(c4, c3, label)))))))
}
/// ```
/// let _ = tek::button_3("", "", "", true);
/// let _ = tek::button_3("", "", "", false);
/// ```
pub fn button_3 <'a> (
key: impl Draw<Tui>,
label: impl Draw<Tui>,
value: impl Draw<Tui>,
editing: bool,
) -> impl Draw<Tui> {
let c1 = tui_orange();
let c2 = tui_g(0);
let c3 = tui_g(96);
let c4 = tui_g(255);
let c5 = tui_g(128);
bold(true, bar(Split::East, 3u16,
fg_bg(c1, c2,
bar(Split::East, 1u16, fg(c2, "▐"),
bar(Split::East, 1u16, key, fg(if editing { c5 } else { c3 }, "▐")))),
east(
when(!editing, east(fg_bg(c4, c3, label), fg_bg(c5, c3, "▐"),)),
east(fg_bg(tui_g(224), c5, value), fg_bg(c5, Reset, "▌"), ))))
}
pub fn field_h <'a, T: Screen> (
_theme: ItemTheme, _head: impl Draw<T>, _body: impl Draw<T>
) -> impl Draw<T> {
}
pub fn field_v <'a, T: Screen> (
_theme: ItemTheme, _head: impl Draw<T>, _body: impl Draw<T>
) -> impl Draw<T> {
}
pub fn view_sessions <'a> () -> impl Draw<Tui> {
let h = 6;
let w = Some(30);
let f = Rgb(224, 192, 128);
draw(move |to: &mut Tui|{
for (index, name) in ["session1", "session2", "session3"].iter().enumerate() {
let b = if index == 0 { Rgb(50,50,50) } else { Rgb(40,40,40) };
let y = (2 * index) as u16;
let h = 2;
bg(b, fg(f, *name).align_w()).full_w().exact_h(h).push_y(y).draw(to)?;
}
Ok(Some(to.area().into()))
}).min_w(w).exact_h(h)
}
/// ```
/// let bg = tengri::ratatui::style::Color::Red;
/// let fg = tengri::ratatui::style::Color::Green;
/// let _ = tek::view_wrap(bg, fg, "and then blue, too!");
/// ```
pub fn view_wrap <'a> (bg: Color, fg: Color, content: impl Draw<Tui>)
-> impl Draw<Tui>
{
let left = fg_bg(bg, Reset, y_repeat("▐").exact_w(1));
let right = fg_bg(bg, Reset, y_repeat("▌").exact_w(1));
east(left, west(right, fg_bg(fg, bg, content)))
}
#[macro_export] macro_rules! rewrite {
($buf:ident, $($rest:tt)*) => {
|$buf,_,_|{
$buf.clear();
write!($buf, $($rest)*)
}
};
}
pub struct Memo<T, U> {
value: T,
view: U,
}
impl<T, U> Memo<T, U> {
pub fn new (value: T, view: U) -> Self {
Self { value, view }
}
}
impl<T: PartialEq + Debug, U: Debug> Debug for Memo<T, U> {
fn fmt (&self, _: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
todo!()
}
}
impl<T: PartialEq, U> Memo<T, U> {
pub fn update <R> (&mut self, newval: T, render: impl Fn(&mut U, &T, &T)->R) -> Option<R> {
if newval != self.value {
let result = render(&mut self.view, &newval, &self.value);
self.value = newval;
return Some(result);
}
None
}
}
pub trait Gettable<T> {
/// Returns current value
fn get (&self) -> T;
}
pub trait Mutable<T>: Gettable<T> {
/// Sets new value, returns old
fn set (&mut self, value: T) -> T;
}
pub trait InteriorMutable<T>: Gettable<T> {
/// Sets new value, returns old
fn set (&self, value: T) -> T;
}
#[cfg(test)] mod test_view {
use super::*;
use proptest::prelude::*;
proptest! {
#[test] fn proptest_view_meter (
label in "\\PC*", value in f32::MIN..f32::MAX
) {
let _ = view_meter(&label, value);
}
#[test] fn proptest_view_meters (
value1 in f32::MIN..f32::MAX,
value2 in f32::MIN..f32::MAX
) {
let _ = view_meters(&[value1, value2]);
}
}
}