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

View file

@ -1,5 +1,34 @@
#![allow(clippy::unit_arg)]
mod deps; pub use self::deps::*;
mod config; pub use self::config::*;
mod config_bind; pub use self::config_bind::*;
mod config_mode; pub use self::config_mode::*;
mod config_view; pub use self::config_view::*;
mod midi_octave; pub use self::midi_octave::*;
mod midi_piano; pub use self::midi_piano::*;
mod midi_point; pub use self::midi_point::*;
mod midi_range; pub use self::midi_range::*;
mod smpl; pub use self::smpl::*;
mod smpl_sample; pub use self::smpl_sample::*;
mod smpl_add; pub use self::smpl_add::*;
mod smpl_kit; pub use self::smpl_kit::*;
mod smpl_voice; pub use self::smpl_voice::*;
mod midi_clip; pub use self::midi_clip::*;
mod midi_edit; pub use self::midi_edit::*;
mod midi_mon; pub use self::midi_mon::*;
mod midi_play; pub use self::midi_play::*;
mod midi_rec; pub use self::midi_rec::*;
mod midi_seq; pub use self::midi_seq::*;
mod time_base; pub use self::time_base::*;
mod time_moment; pub use self::time_moment::*;
mod time_ticker; pub use self::time_ticker::*;
/// Define a type alias for iterators of sized items (columns).
macro_rules! def_sizes_iter {
($Type:ident => $($Item:ty),+) => {
@ -8,57 +37,24 @@ macro_rules! def_sizes_iter {
}
}
mod app; pub use self::app::*;
mod browse; pub use self::browse::*;
mod cli; pub use self::cli::*;
mod clock; pub use self::clock::*;
mod clock_view; pub use self::clock_view::*;
mod config; pub use self::config::*;
mod config_bind; pub use self::config_bind::*;
mod config_mode; pub use self::config_mode::*;
mod config_view; pub use self::config_view::*;
mod deps; pub use self::deps::*;
mod device; pub use self::device::*;
mod dialog; pub use self::dialog::*;
mod field; pub use self::field::*;
mod memo; pub use self::memo::*;
mod menu_item; pub use self::menu_item::*;
mod meter; pub use self::meter::*;
mod mix; pub use self::mix::*;
mod moment; pub use self::moment::*;
mod octave; pub use self::octave::*;
mod piano; pub use self::piano::*;
mod point; pub use self::point::*;
mod pool; pub use self::pool::*;
mod port; pub use self::port::*;
mod project; pub use self::project::*;
mod range; pub use self::range::*;
mod sampler; pub use self::sampler::*;
mod sample; pub use self::sample::*;
mod sample_add; pub use self::sample_add::*;
mod sample_kit; pub use self::sample_kit::*;
mod sample_voice; pub use self::sample_voice::*;
mod scene; pub use self::scene::*;
mod select; pub use self::select::*;
mod midi_clip; pub use self::midi_clip::*;
mod midi_clips; pub use self::midi_clips::*;
mod midi_edit; pub use self::midi_edit::*;
mod midi_mon; pub use self::midi_mon::*;
mod midi_play; pub use self::midi_play::*;
mod midi_rec; pub use self::midi_rec::*;
mod midi_seq; pub use self::midi_seq::*;
mod ticker; pub use self::ticker::*;
mod timebase; pub use self::timebase::*;
mod track; pub use self::track::*;
mod proj; pub use self::proj::*;
mod proj_clock; pub use self::proj_clock::*;
mod proj_device; pub use self::proj_device::*;
mod proj_mixer; pub use self::proj_mixer::*;
mod proj_port; pub use self::proj_port::*;
mod proj_scene; pub use self::proj_scene::*;
mod proj_select; pub use self::proj_select::*;
mod proj_track; pub use self::proj_track::*;
mod proj_pool; pub use self::proj_pool::*;
#[cfg(feature = "lv2")] mod lv2;
#[cfg(feature = "lv2")] pub use self::lv2::*;
mod tek_browse; pub use self::tek_browse::*;
mod tek_cli; pub use self::tek_cli::*;
mod tek_ctrl; pub use self::tek_ctrl::*;
mod tek_view; pub use self::tek_view::*;
#[cfg(feature = "lv2_gui")] mod lv2_gui;
#[cfg(feature = "lv2_gui")] pub use self::lv2_gui::*;
#[cfg(feature = "vst2")] mod vst2;
#[cfg(feature = "vst2")] pub use self::vst2::*;
#[cfg(feature = "lv2")] mod device_lv2; #[cfg(feature = "lv2")] pub use self::device_lv2::*;
#[cfg(feature = "lv2_gui")] mod device_lv2_gui; #[cfg(feature = "lv2_gui")] pub use self::device_lv2_gui::*;
#[cfg(feature = "vst2")] mod device_vst2; #[cfg(feature = "vst2")] pub use self::device_vst2::*;
/// Command-line entrypoint.
#[allow(unused)]
@ -102,3 +98,689 @@ fn run_new_plain (config: Config) -> Usually<()> {
Ok(app)
})?)
}
/// Total application state.
///
/// ```
/// use tek::{HasTracks, HasScenes, TracksView, ScenesView};
/// let mut app = tek::App::default();
/// let _ = app.scenes_add_one(None, None).unwrap();
/// let _ = app.update_clock();
/// app.project.editor = Some(Default::default());
/// //let _: Vec<_> = app.project.inputs_with_sizes().collect();
/// //let _: Vec<_> = app.project.outputs_with_sizes().collect();
/// let _: Vec<_> = app.project.tracks_with_sizes().collect();
/// //let _: Vec<_> = app.project.scenes_with_sizes(true, 10, 10).collect();
/// //let _: Vec<_> = app.scenes_with_colors(true, 10).collect();
/// //let _: Vec<_> = app.scenes_with_track_colors(true, 10, 10).collect();
/// let _ = app.project.w();
/// //let _ = app.project.w_sidebar();
/// //let _ = app.project.w_tracks_area();
/// let _ = app.project.h();
/// //let _ = app.project.h_tracks_area();
/// //let _ = app.project.h_inputs();
/// //let _ = app.project.h_outputs();
/// let _ = app.project.h_scenes();
/// ```
#[derive(Default, Debug)]
#[namespace(Arc<[Connect]> get_arc_array_connect)]
#[namespace(Arc<str> get_arc_str)]
#[namespace(Color get_color)]
#[namespace(ControlAxis get_axis)]
#[namespace(Dialog get_dialog)]
#[namespace(ItemColor)]
#[namespace(ItemTheme)]
#[namespace(MidiClip)]
#[namespace(Option<Arc<[Connect]>> get_opt_arc_array_connect)]
#[namespace(Option<Arc<str>> get_opt_arc_str)]
#[namespace(Option<ItemTheme> get_opt_itemtheme)]
#[namespace(Option<Arc<RwLock<MidiClip>>> get_opt_arc_rwlock_midiclip)]
#[namespace(Option<Vec<Option<Arc<RwLock<MidiClip>>>>> get_opt_vec_opt_arc_rwlock_midiclip)]
#[namespace(Option<bool> get_opt_bool)]
#[namespace(Option<u16> get_opt_u16)]
#[namespace(Option<u32> get_opt_u32)]
#[namespace(Option<usize> get_opt_usize)]
#[namespace(PathBuf get_path_buf)]
#[namespace(Vec<Arc<str>> get_vec_arc_str)]
#[namespace(bool get_bool)]
#[namespace(f64 get_f64)]
#[namespace(isize)]
#[namespace(u16 get_u16)]
#[namespace(u32)]
#[namespace(u8)]
#[namespace(usize get_usize)]
pub struct App {
/// Exit flag
pub exit: Exit,
/// Base color.
pub color: ItemTheme,
/// Must not be dropped for the duration of the process
pub jack: Jack<'static>,
/// Display size
pub size: Sizer,
/// Performance counter
pub perf: PerfModel,
/// Available view modes and input bindings
pub config: Arc<Config>,
/// Currently selected mode
pub modes: Arc<RwLock<Vec<Arc<str>>>>,
/// Undo history
pub history: Vec<(AppCmd, Option<AppCmd>)>,
/// Dialog overlay
pub dialog: Dialog,
/// Contains the currently edited musical arrangement
pub project: Project,
/// Error, if any
pub error: Arc<RwLock<Option<Arc<str>>>>,
#[cfg(feature = "prof2")]
/// Tracing guard
pub guard: Option<tracing_flame::FlushGuard<std::io::BufWriter<std::fs::File>>>,
}
impl App {
/// Create a new application instance from a backend, project, config, and mode
///
/// ```
/// let mut proj = tek::Project::default();
/// proj.jack = tek::tengri::Jack::new(&"test_tek").expect("failed to connect to jack");
/// let config = "(mode hi (name Hello) (info World))";
/// let mut conf = tek::Config::load(tek::Config::default(), config).unwrap();
/// let tek = tek::App::new(None, proj, conf.into(), "hello");
/// ```
pub fn new (
exit: Option<Exit>,
project: Project,
config: Arc<Config>,
mode: impl AsRef<str>
) -> Self {
App {
exit: exit.unwrap_or_default(),
jack: project.jack.clone(),
color: ItemTheme::random(),
dialog: Dialog::welcome(),
modes: Arc::new(RwLock::new(vec![mode.as_ref().into()])),
config,
project,
..Default::default()
}
}
}
fn get_arc_str <'a> (_: &App, src: impl Language) -> Perhaps<Arc<str>> {
Ok(src.src()?.map(|x|x.into()))
}
fn get_u16 <'a> (state: &App, src: impl Language) -> Perhaps<u16> {
Ok(Some(match src.word()? {
Some(":w/sidebar") => state.project.w_sidebar(state.editor().is_some()),
Some(":h/sample-detail") => 6.max(state.size.h() as u16 * 3 / 9),
_ => return try_to_u16(src)
}))
}
fn get_usize <'a> (state: &App, src: impl Language) -> Perhaps<usize> {
Ok(Some(match src.word()? {
Some(":scene-count") => state.scenes().len(),
Some(":track-count") => state.tracks().len(),
Some(":device-kind") => state.dialog.device_kind().unwrap_or(0),
Some(":device-kind/next") => state.dialog.device_kind_next().unwrap_or(0),
Some(":device-kind/prev") => state.dialog.device_kind_prev().unwrap_or(0),
_ => return try_to_usize(src)
}))
}
fn get_f64 <'a> (_state: &App, _src: impl Language) -> Perhaps<f64> {
todo!()
}
fn get_bool <'a> (state: &App, src: impl Language) -> Perhaps<bool> {
src.word()?.map(|word|Ok(match word {
"Y" => true,
"N" => false,
":mode/editor" => state.project.editor.is_some(),
":focused/dialog" => !matches!(state.dialog, Dialog::None),
":focused/message" => matches!(state.dialog, Dialog::Message(..)),
":focused/add_device" => matches!(state.dialog, Dialog::Device(..)),
":focused/browser" => state.dialog.browser().is_some(),
":focused/pool/import" => matches!(state.project.pool.mode, Some(PoolMode::Import(..))),
":focused/pool/export" => matches!(state.project.pool.mode, Some(PoolMode::Export(..))),
":focused/pool/rename" => matches!(state.project.pool.mode, Some(PoolMode::Rename(..))),
":focused/pool/length" => matches!(state.project.pool.mode, Some(PoolMode::Length(..))),
":focused/clip" => !state.editor_focused() && matches!(state.selection(), Selection::TrackClip{..}),
":focused/track" => !state.editor_focused() && matches!(state.selection(), Selection::Track(..)),
":focused/scene" => !state.editor_focused() && matches!(state.selection(), Selection::Scene(..)),
":focused/mix" => !state.editor_focused() && matches!(state.selection(), Selection::Mix),
_ => return err!("not bool: {word}")
})).transpose()
}
#[allow(unused)]
fn get_selection <'a> (state: &App, src: impl Language) -> Perhaps<Selection> {
src.word()?.map(|word|Ok(match word {
":select/scene" => state.selection().select_scene(state.tracks().len()),
":select/scene/next" => state.selection().select_scene_next(state.scenes().len()),
":select/scene/prev" => state.selection().select_scene_prev(),
":select/track" => state.selection().select_track(state.tracks().len()),
":select/track/next" => state.selection().select_track_next(state.tracks().len()),
":select/track/prev" => state.selection().select_track_prev(),
_ => return err!("not selection: {word}")
})).transpose()
}
fn get_color <'a> (_: &App, src: impl Language) -> Perhaps<Color> {
if let Some(expr) = src.expr()? {
match (expr.head()?, expr.tail()?) {
(Some("g"), Some(tail)) => {
let n = try_to_u8(tail.head().map_err(Into::into))?
.ok_or(LanguageError::domain("not gray"))?;
Ok(Some(Color::Rgb(n, n, n)))
},
(Some("rgb"), Some(tail)) => {
let r = try_to_u8(tail.head().map_err(Into::into))?
.ok_or(LanguageError::domain("not red"))?;
let g = try_to_u8(tail.tail().head().map_err(Into::into))?
.ok_or(LanguageError::domain("not green"))?;
let b = try_to_u8(tail.tail().tail().head().map_err(Into::into))?
.ok_or(LanguageError::domain("not blue"))?;
Ok(Some(Color::Rgb(r, g, b)))
},
(Some(_), _) => return err!("not a color expression: {expr}"),
(None, _) => return err!("not a color expression: {expr}"),
}
} else if let Ok(Some(sym)) = src.word() {
Ok(match sym {
":color/bg" => Some(Color::Rgb(28, 32, 36)),
":color/fg" => Some(Color::Rgb(98, 92, 96)),
_ => return err!("not a color: {sym}")
})
} else {
return err!("not a color: {:?}", src.src()?)
}
}
#[allow(unused)]
fn get_opt_u7 <'a> (state: &App, src: impl Language) -> Perhaps<Option<u7>> {
eval_word(src, |word|Ok(match word {
":editor/pitch" => Some((
state.editor().as_ref().map(|e|e.get_note_pos()).unwrap() as u8
).into()),
_ => return err!("unknown midi note: {word}")
}))
}
fn get_opt_usize <'a> (state: &App, src: impl Language) -> Perhaps<Option<usize>> {
eval_word(src, |word|Ok(match word {
":selected/scene" => state.selection().scene(),
":selected/track" => state.selection().track(),
_ => return err!("unknown opt<usize>: {word}")
}))
}
fn get_axis <'a> (_: &App, src: impl Language) -> Perhaps<ControlAxis> {
eval_word(src, |word|Ok(match word {
"X" => ControlAxis::X, "XFine" | "X2" => ControlAxis::XFine,
"Y" => ControlAxis::Y, "YFine" | "Y2" => ControlAxis::YFine,
"Z" => ControlAxis::Z, "ZFine" | "Z2" => ControlAxis::ZFine,
_ => return err!("unknown axis {word}")
}))
}
fn get_opt_u16 <'a> (_: &App, _src: impl Language) -> Perhaps<Option<u16>> {
Ok(None)
}
fn get_opt_u32 <'a> (_: &App, _src: impl Language) -> Perhaps<Option<u32>> {
Ok(None)
}
fn get_opt_itemtheme <'a> (_: &App, _: impl Language) -> Perhaps<Option<ItemTheme>> {
todo!()
}
fn get_opt_arc_array_connect <'a> (_: &App, _: impl Language) -> Perhaps<Option<Arc<[Connect]>>> {
todo!()
}
fn get_arc_array_connect <'a> (_: &App, _: impl Language) -> Perhaps<Arc<[Connect]>> {
todo!()
}
fn get_opt_arc_str <'a> (_: &App, _: impl Language) -> Perhaps<Option<Arc<str>>> {
todo!()
}
fn get_opt_bool <'a> (_: &App, _: impl Language) -> Perhaps<Option<bool>> {
todo!()
}
fn get_path_buf <'a> (_: &App, _: impl Language) -> Perhaps<PathBuf> {
todo!()
}
fn get_vec_arc_str <'a> (_: &App, _: impl Language) -> Perhaps<Vec<Arc<str>>> {
todo!()
}
impl_has!(Clock: |self: App|self.project.clock);
impl_has!(Dialog: |self: App|self.dialog);
impl_has!(Jack<'static>: |self: App|self.jack);
impl_has!(Pool: |self: App|self.project.pool);
impl_has!(Selection: |self: App|self.project.selection);
impl_as_ref!(Option<MidiEditor>: |self: App|self.project.as_ref());
impl_as_mut!(Option<MidiEditor>: |self: App|self.project.as_mut());
primitive!(u8: try_to_u8);
primitive!(u16: try_to_u16);
primitive!(usize: try_to_usize);
primitive!(isize: try_to_isize);
impl HasClips for App {
fn clip (&self) -> Option<Arc<RwLock<MidiClip>>> {
self.project.clip()
}
fn clip_mut (&self) -> &mut Option<Arc<RwLock<MidiClip>>> {
self.project.clip_mut()
}
fn clips <'a> (&'a self) -> RwLockReadGuard<'a, Clips> {
self.project.clips()
}
fn clips_mut <'a> (&'a self) -> RwLockWriteGuard<'a, Clips> {
self.project.clips_mut()
}
}
tui_keys!(self: App, input {
#[cfg(feature = "prof2")] profiling::scope!("App::tui_keys!");
Self::input_cmd(self, input)
});
impl App {
fn input_cmd (state: &mut Self, input: &TuiEvent) -> Usually<()> {
if let Some(bindings) = Self::input_cmd_find(state, input) {
return Self::input_cmd_exec(state, bindings)
}
Ok(())
}
fn input_cmd_find (state: &mut Self, input: &TuiEvent) -> Option<Arc<[Binding<Arc<str>>]>> {
for name in state.modes.read().iter().rev() {
let mode = state.config.get_mode(name).as_ref().map(Arc::clone);
if let Some(mode) = mode {
let binds = state.config.binds.clone();
for id in mode.keys.iter() {
if let Some(event_map) = binds.find(id)
&& let Some(bindings) = event_map.query(input) {
return Some(bindings.into())
}
}
}
}
None
}
fn input_cmd_exec (state: &mut Self, bindings: Arc<[Binding<Arc<str>>]>) -> Usually<()> {
for binding in bindings.iter() {
for cmd in binding.commands.iter() {
if Self::cmd::<AppCmd>(state, cmd)? {
return Ok(())
}
}
}
Ok(())
}
fn cmd <
T: Dispatch<Self> + Clone + Into<AppCmd>
> (state: &mut Self, src: &str) -> Usually<bool> where
Self: Namespace<T>,
{
if let Some(cmd) = Namespace::<T>::namespace(state, &src)? {
return Self::cmd_dispatch(state, cmd.into())
}
Ok(false)
}
fn cmd_dispatch (state: &mut Self, cmd: AppCmd) -> Usually<bool> where
Self: Namespace<AppCmd>,
{
match cmd.clone().dispatch(state) {
Err(err) => {
state.history.push((cmd.into(), None));
return Err(err)
},
Ok(undo) => {
state.history.push((cmd.into(), undo.map(Into::into)));
return Ok(true)
}
}
}
}
/// A control axis.
///
/// ```
/// let axis = tek::ControlAxis::X;
/// ```
#[derive(Debug, Copy, Clone)] pub enum ControlAxis {
X, XFine,
Y, YFine,
Z, ZFine,
I, IFine,
}
/// The [Draw] implementation for [App] handles the loaded view,
/// which is defined in terms of [dizzle] DSL.
///
/// If there is an error, the error is displayed. FIXME: overlay it
/// Then, every top-level form of the DSL description is rendered.
impl Draw<Tui> for App {
fn draw (&self, to: &mut Tui) -> Drawn<u16> {
#[cfg(feature = "prof2")] profiling::scope!("App::draw");
//self.perf.cycle(&mut |_|{
self.draw_error(to)?;
self.draw_modes(to)?;
self.draw_debug(to)?;
#[cfg(feature = "prof2")] profiling::finish_frame!();
Ok(Some(to.area().into()))
//})
}
}
impl App {
fn draw_error (&self, to: &mut Tui) -> Usually<()> {
if let Some(e) = self.error.try_read().unwrap().as_ref() {
e.as_ref().align_c().draw(to)?;
}
Ok(())
}
fn draw_modes (&self, to: &mut Tui) -> Usually<()> {
for name in self.modes.read().iter() {
let mut error = false;
if let Some(mode) = self.config.get_mode(name).as_ref().map(Arc::clone) {
for (index, view) in mode.view.iter().enumerate() {
match (view.render)(self, to) {
Ok(None) => {},
Ok(Some(XYWH(.., w, h))) => {
self.size.0.store(w as usize, Relaxed);
self.size.1.store(h as usize, Relaxed);
},
Err(e) => {
let message = format!(
"Mode: {:?}\n\nLayer: #{index}\n\nError: {e}\n\nSource:\n{}",
&mode.name,
&view.source
);
*self.error.try_write().unwrap() = Some(message.into());
error = true;
break;
}
}
}
if error {
break
} else {
*self.error.try_write().unwrap() = None;
}
}
}
Ok(())
}
#[allow(unused)] fn draw_debug (&self, to: &mut Tui) -> Drawn<u16> {
Split::Above.bar(0,
Split::West.bar(16,
format!("{}/{} {} ",
self.perf.used.load(Relaxed),
self.perf.window.load(Relaxed),
self.perf.clock.raw() / 10000000).align_e(),
format!("{}x{} ",
self.size.0.load(Relaxed),
self.size.1.load(Relaxed)).align_w(),
).align_se(),
Split::East.bar(16,
format!("{}", self.modes.read()[0]),
format!("{}", self.modes.read()[0])
).align_sw()
).draw(to)
}
}
impl_has!(Sizer: |self: App|self.size);
pub trait HasWidth {
const MIN_WIDTH: usize;
/// Increment track width.
fn width_inc (&mut self);
/// Decrement track width, down to a hardcoded minimum of [Self::MIN_WIDTH].
fn width_dec (&mut self);
}
pub fn get_dialog <'a> (_: &App, src: impl Language) -> Perhaps<Dialog> {
eval_word(&src, |word|Ok(match word {
":none" => Dialog::None,
":options" => Dialog::Options,
":device" => Dialog::Device(0),
":device/prev" => Dialog::Device(0),
":device/next" => Dialog::Device(0),
":help" => Dialog::Help(0),
":save" => Dialog::Browse(
BrowseTarget::SaveProject,
Browse::new(None).unwrap().into()),
":load" => Dialog::Browse(
BrowseTarget::LoadProject,
Browse::new(None).unwrap().into()),
":import/clip" => Dialog::Browse(
BrowseTarget::ImportClip(Default::default()),
Browse::new(None).unwrap().into()),
":export/clip" => Dialog::Browse(
BrowseTarget::ExportClip(Default::default()),
Browse::new(None).unwrap().into()),
":import/sample" => Dialog::Browse(
BrowseTarget::ImportSample(Default::default()),
Browse::new(None).unwrap().into()),
":export/sample" => Dialog::Browse(
BrowseTarget::ExportSample(Default::default()),
Browse::new(None).unwrap().into()),
_ => return Err(format!("not a dialog: {}", src.src()?.unwrap_or("<???>")).into())
}))
}
/// Various possible dialog modes.
///
/// ```
/// let dialog: tek::Dialog = Default::default();
/// ```
#[derive(Debug, Clone, Default, PartialEq)] pub enum Dialog {
#[default] None,
Help(usize),
Menu(usize, Arc<[MenuItem]>),
Device(usize),
Message(Arc<str>),
Browse(BrowseTarget, Arc<Browse>),
Options,
}
impl Dialog {
/// ```
/// let _ = tek::Dialog::welcome();
/// ```
pub fn welcome () -> Self {
Self::Menu(1, Arc::new([
MenuItem::new(" Resume session ", [
AppCmd::Project {
command: ProjectCmd::LoadLast
}
]),
MenuItem::new(" Create session ", [
AppCmd::Dialog {
dialog: Dialog::None
},
AppCmd::Mode {
command: ModeCmd::Reset {
modes: vec!["Arranger".into()]
}
},
AppCmd::Tracks {
command: TracksCmd::Add
},
AppCmd::Scenes {
command: ScenesCmd::Add
}
]),
MenuItem::new(" Load session ", [
AppCmd::Dialog {
dialog: Dialog::Browse(
BrowseTarget::LoadProject,
Browse::new(None).unwrap().into()
)
//dialog: Dialog::Browse {
//title: "Load session".into(),
//filter: "\\.tek$",
//}
}
]),
MenuItem::new(" Exit ", [
AppCmd::Exit
]),
]))
}
/// FIXME: generalize
/// ```
/// let _ = tek::Dialog::welcome().menu_selected();
/// ```
pub fn menu_selected (&self) -> Option<usize> {
if let Self::Menu(selected, _) = self { Some(*selected) } else { None }
}
/// FIXME: generalize
/// ```
/// let _ = tek::Dialog::welcome().menu_next();
/// ```
pub fn menu_next (&self) -> Self {
match self {
Self::Menu(index, items) => Self::Menu(wrap_inc(*index, items.len()), items.clone()),
_ => Self::None
}
}
/// FIXME: generalize
/// ```
/// let _ = tek::Dialog::welcome().menu_prev();
/// ```
pub fn menu_prev (&self) -> Self {
match self {
Self::Menu(index, items) => Self::Menu(wrap_dec(*index, items.len()), items.clone()),
_ => Self::None
}
}
/// FIXME: generalize
/// ```
/// let _ = tek::Dialog::welcome().device_kind();
/// ```
pub fn device_kind (&self) -> Option<usize> {
if let Self::Device(index) = self { Some(*index) } else { None }
}
/// FIXME: generalize
/// ```
/// let _ = tek::Dialog::welcome().device_kind_next();
/// ```
pub fn device_kind_next (&self) -> Option<usize> {
self.device_kind().map(|index|(index + 1) % device_kinds().len())
}
/// FIXME: generalize
/// ```
/// let _ = tek::Dialog::welcome().device_kind_prev();
/// ```
pub fn device_kind_prev (&self) -> Option<usize> {
self.device_kind().map(|index|index.overflowing_sub(1).0.min(device_kinds().len().saturating_sub(1)))
}
/// FIXME: implement
pub fn message (&self) -> Option<&str> { todo!() }
/// FIXME: implement
pub fn browser (&self) -> Option<&Arc<Browse>> { todo!() }
/// FIXME: implement
pub fn browser_target (&self) -> Option<&BrowseTarget> { todo!() }
}
/// Increment a wrapping counter.
pub const fn wrap_inc (index: usize, count: usize) -> usize {
if count > 0 { (index + 1) % count } else { 0 }
}
/// Decrement a wrapping counter.
pub const fn wrap_dec (index: usize, count: usize) -> usize {
if count > 0 {
let a = index.overflowing_sub(1).0;
let b = count.saturating_sub(1);
if a < b { a } else { b }
} else {
0
}
}
/// An item of a menu.
///
/// ```
/// let item: tek::MenuItem = Default::default();
/// ```
#[derive(Clone)] pub struct MenuItem(
pub Arc<str>,
pub Arc<[AppCmd]>,
);
impl MenuItem {
pub fn new <const N: usize> (name: impl AsRef<str>, actions: [AppCmd; N]) -> Self {
Self(name.as_ref().into(), Arc::new(actions))
}
}
impl_debug!(MenuItem |self, w| { write!(w, "{}", &self.0) });
impl_default!(MenuItem: Self::new("", []));
impl PartialEq for MenuItem {
fn eq (&self, other: &Self) -> bool {
self.0 == other.0
}
}
//impl AsRef<Arc<[MenuItem]>> for MenuItems {
//fn as_ref (&self) -> &Arc<[MenuItem]> {
//&self.0
//}
//}