mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-07 12:16:42 +01:00
wip: p.45, e=33, lotta todo!
This commit is contained in:
parent
260736f31d
commit
627c7d8820
17 changed files with 198 additions and 226 deletions
|
|
@ -25,8 +25,8 @@ pub struct ArrangerTui {
|
|||
pub midi_buf: Vec<Vec<Vec<u8>>>,
|
||||
pub cursor: (usize, usize),
|
||||
pub menu_bar: Option<MenuBar<Tui, Self, ArrangerCommand>>,
|
||||
pub status_bar: Option<S>,
|
||||
pub history: Vec<C>,
|
||||
pub status_bar: Option<ArrangerStatus>,
|
||||
pub history: Vec<ArrangerCommand>,
|
||||
}
|
||||
|
||||
impl TryFrom<&Arc<RwLock<JackClient>>> for ArrangerTui {
|
||||
|
|
|
|||
|
|
@ -20,13 +20,19 @@ pub enum ArrangerCommand {
|
|||
Clip(ArrangerClipCommand),
|
||||
Select(ArrangerSelection),
|
||||
Zoom(usize),
|
||||
Phrases(PhrasePoolViewCommand),
|
||||
Phrases(PhrasePoolCommand),
|
||||
Editor(PhraseEditorCommand),
|
||||
EditPhrase(Option<Arc<RwLock<Phrase>>>),
|
||||
}
|
||||
|
||||
impl InputToCommand<Tui, ArrangerTui> for ArrangerCommand {
|
||||
fn input_to_command (view: &ArrangerTui, input: &TuiInput) -> Option<Self> {
|
||||
pub trait ArrangerControl {
|
||||
}
|
||||
|
||||
impl ArrangerControl for ArrangerTui {
|
||||
}
|
||||
|
||||
impl<T: ArrangerControl> InputToCommand<Tui, T> for ArrangerCommand {
|
||||
fn input_to_command (view: &T, input: &TuiInput) -> Option<Self> {
|
||||
use FocusCommand::*;
|
||||
use ArrangerCommand::*;
|
||||
Some(match input.event() {
|
||||
|
|
@ -44,7 +50,7 @@ impl InputToCommand<Tui, ArrangerTui> for ArrangerCommand {
|
|||
Self::App(Playhead(PlayheadCommand::Play(None)))
|
||||
},
|
||||
_ => Self::App(match view.focused() {
|
||||
Content(ArrangerFocus::Transport) => {
|
||||
ArrangerFocus::Transport => {
|
||||
use TransportCommand::{Clock, Playhead};
|
||||
match TransportCommand::input_to_command(view, input)? {
|
||||
Clock(command) => {
|
||||
|
|
@ -55,18 +61,18 @@ impl InputToCommand<Tui, ArrangerTui> for ArrangerCommand {
|
|||
},
|
||||
}
|
||||
},
|
||||
Content(ArrangerFocus::PhraseEditor) => Editor(
|
||||
PhraseEditorCommand::input_to_command(&view.editor, input)?
|
||||
ArrangerFocus::PhraseEditor => Editor(
|
||||
PhraseCommand::input_to_command(&view.editor, input)?
|
||||
),
|
||||
Content(ArrangerFocus::PhrasePool) => match input.event() {
|
||||
ArrangerFocus::PhrasePool => match input.event() {
|
||||
key!(KeyCode::Char('e')) => EditPhrase(
|
||||
Some(view.phrase().clone())
|
||||
),
|
||||
_ => Phrases(
|
||||
PhrasePoolViewCommand::input_to_command(view, input)?
|
||||
PhrasePoolCommand::input_to_command(view, input)?
|
||||
)
|
||||
},
|
||||
Content(ArrangerFocus::Arranger) => {
|
||||
ArrangerFocus::Arranger => {
|
||||
use ArrangerSelection as Select;
|
||||
use ArrangerTrackCommand as Track;
|
||||
use ArrangerClipCommand as Clip;
|
||||
|
|
@ -183,23 +189,11 @@ impl InputToCommand<Tui, ArrangerTui> for ArrangerCommand {
|
|||
}
|
||||
}
|
||||
|
||||
impl Command<ArrangerTui> for ArrangerCommand {
|
||||
fn execute (self, state: &mut ArrangerTui) -> Perhaps<Self> {
|
||||
let undo = match self {
|
||||
Focus(cmd) => { delegate(cmd, Focus, state) },
|
||||
App(cmd) => { delegate(cmd, App, state) }
|
||||
_ => {todo!()}
|
||||
}?;
|
||||
state.show_phrase();
|
||||
state.update_status();
|
||||
return Ok(undo);
|
||||
}
|
||||
}
|
||||
|
||||
impl Command<ArrangerTui> for ArrangerCommand {
|
||||
fn execute (self, state: &mut ArrangerTui) -> Perhaps<Self> {
|
||||
impl<T: ArrangerControl> Command<T> for ArrangerCommand {
|
||||
fn execute (self, state: &mut T) -> Perhaps<Self> {
|
||||
use ArrangerCommand::*;
|
||||
match self {
|
||||
Focus(cmd) => { delegate(cmd, Focus, state) },
|
||||
Scene(cmd) => { delegate(cmd, Scene, &mut state) },
|
||||
Track(cmd) => { delegate(cmd, Track, &mut state) },
|
||||
Clip(cmd) => { delegate(cmd, Clip, &mut state) },
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ use crate::*;
|
|||
/// Sections in the arranger app that may be focused
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
|
||||
pub enum ArrangerFocus {
|
||||
/// The menu bar is focused
|
||||
Menu,
|
||||
/// The transport (toolbar) is focused
|
||||
Transport,
|
||||
/// The arrangement (grid) is focused
|
||||
|
|
@ -13,16 +15,15 @@ pub enum ArrangerFocus {
|
|||
PhraseEditor,
|
||||
}
|
||||
|
||||
impl FocusEnter for ArrangerApp<Tui> {
|
||||
type Item = AppViewFocus<ArrangerFocus>;
|
||||
impl FocusEnter for ArrangerTui {
|
||||
type Item = ArrangerFocus;
|
||||
fn focus_enter (&mut self) {
|
||||
use AppViewFocus::*;
|
||||
use ArrangerFocus::*;
|
||||
let focused = self.focused();
|
||||
if !self.entered {
|
||||
self.entered = focused == Content(Arranger);
|
||||
self.app.editor.entered = focused == Content(PhraseEditor);
|
||||
self.app.phrases.entered = focused == Content(PhrasePool);
|
||||
self.entered = focused == Arranger;
|
||||
self.app.editor.entered = focused == PhraseEditor;
|
||||
self.app.phrases.entered = focused == PhrasePool;
|
||||
}
|
||||
}
|
||||
fn focus_exit (&mut self) {
|
||||
|
|
@ -42,8 +43,8 @@ impl FocusEnter for ArrangerApp<Tui> {
|
|||
}
|
||||
|
||||
/// Focus layout of arranger app
|
||||
impl FocusGrid for ArrangerApp<Tui> {
|
||||
type Item = AppViewFocus<ArrangerFocus>;
|
||||
impl FocusGrid for ArrangerTui {
|
||||
type Item = ArrangerFocus;
|
||||
fn focus_cursor (&self) -> (usize, usize) {
|
||||
self.cursor
|
||||
}
|
||||
|
|
@ -51,24 +52,22 @@ impl FocusGrid for ArrangerApp<Tui> {
|
|||
&mut self.cursor
|
||||
}
|
||||
fn focus_layout (&self) -> &[&[Self::Item]] {
|
||||
use AppViewFocus::*;
|
||||
use ArrangerFocus::*;
|
||||
&[
|
||||
&[Menu, Menu ],
|
||||
&[Content(Transport), Content(Transport) ],
|
||||
&[Content(Arranger), Content(Arranger) ],
|
||||
&[Content(PhrasePool), Content(PhraseEditor)],
|
||||
&[Transport, Transport ],
|
||||
&[Arranger, Arranger ],
|
||||
&[PhrasePool, PhraseEditor],
|
||||
]
|
||||
}
|
||||
fn focus_update (&mut self) {
|
||||
use AppViewFocus::*;
|
||||
use ArrangerFocus::*;
|
||||
let focused = self.focused();
|
||||
if let Some(mut status_bar) = self.status_bar {
|
||||
status_bar.update(&(
|
||||
self.focused(),
|
||||
self.app.selected,
|
||||
focused == Content(PhraseEditor) && self.entered
|
||||
focused == PhraseEditor && self.entered
|
||||
))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use crate::*;
|
||||
|
||||
impl HasScenes<ArrangerScene> for ArrangerView<Tui> {
|
||||
impl HasScenes<ArrangerScene> for ArrangerTui {
|
||||
fn scenes (&self) -> &Vec<ArrangerScene> {
|
||||
&self.scenes
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use crate::*;
|
|||
|
||||
/// Status bar for arranger app
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum ArrangerStatusBar {
|
||||
pub enum ArrangerStatus {
|
||||
Transport,
|
||||
ArrangerMix,
|
||||
ArrangerTrack,
|
||||
|
|
@ -13,35 +13,30 @@ pub enum ArrangerStatusBar {
|
|||
PhraseEdit,
|
||||
}
|
||||
|
||||
impl StatusBar for ArrangerStatusBar {
|
||||
|
||||
type State = (AppViewFocus<ArrangerFocus>, ArrangerSelection, bool);
|
||||
|
||||
impl StatusBar for ArrangerStatus {
|
||||
type State = (ArrangerFocus, ArrangerSelection, bool);
|
||||
fn hotkey_fg () -> Color where Self: Sized {
|
||||
TuiTheme::hotkey_fg()
|
||||
}
|
||||
fn update (&mut self, (focused, selected, entered): &Self::State) {
|
||||
use AppViewFocus::*;
|
||||
if let Content(focused) = focused {
|
||||
*self = match focused {
|
||||
ArrangerFocus::Transport => ArrangerStatusBar::Transport,
|
||||
ArrangerFocus::Transport => ArrangerStatus::Transport,
|
||||
ArrangerFocus::Arranger => match selected {
|
||||
ArrangerSelection::Mix => ArrangerStatusBar::ArrangerMix,
|
||||
ArrangerSelection::Track(_) => ArrangerStatusBar::ArrangerTrack,
|
||||
ArrangerSelection::Scene(_) => ArrangerStatusBar::ArrangerScene,
|
||||
ArrangerSelection::Clip(_, _) => ArrangerStatusBar::ArrangerClip,
|
||||
ArrangerSelection::Mix => ArrangerStatus::ArrangerMix,
|
||||
ArrangerSelection::Track(_) => ArrangerStatus::ArrangerTrack,
|
||||
ArrangerSelection::Scene(_) => ArrangerStatus::ArrangerScene,
|
||||
ArrangerSelection::Clip(_, _) => ArrangerStatus::ArrangerClip,
|
||||
},
|
||||
ArrangerFocus::PhrasePool => ArrangerStatusBar::PhrasePool,
|
||||
ArrangerFocus::PhrasePool => ArrangerStatus::PhrasePool,
|
||||
ArrangerFocus::PhraseEditor => match entered {
|
||||
true => ArrangerStatusBar::PhraseEdit,
|
||||
false => ArrangerStatusBar::PhraseView,
|
||||
true => ArrangerStatus::PhraseEdit,
|
||||
false => ArrangerStatus::PhraseView,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Content for ArrangerStatusBar {
|
||||
impl Content for ArrangerStatus {
|
||||
type Engine = Tui;
|
||||
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||
let label = match self {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use crate::*;
|
||||
|
||||
impl HasTracks<ArrangerTrack> for ArrangerView<Tui> {
|
||||
impl HasTracks<ArrangerTrack> for ArrangerTui {
|
||||
fn tracks (&self) -> &Vec<ArrangerTrack> {
|
||||
&self.tracks
|
||||
}
|
||||
|
|
@ -9,7 +9,7 @@ impl HasTracks<ArrangerTrack> for ArrangerView<Tui> {
|
|||
}
|
||||
}
|
||||
|
||||
impl ArrangerTracksApi<ArrangerTrack> for ArrangerView<Tui> {
|
||||
impl ArrangerTracksApi<ArrangerTrack> for ArrangerTui {
|
||||
fn track_add (&mut self, name: Option<&str>, color: Option<ItemColor>)
|
||||
-> Usually<&mut ArrangerTrack>
|
||||
{
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ impl ArrangerMode {
|
|||
}
|
||||
|
||||
/// Layout for standalone arranger app.
|
||||
impl Content for ArrangerView<Tui> {
|
||||
impl Content for ArrangerTui {
|
||||
type Engine = Tui;
|
||||
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||
Split::up(
|
||||
|
|
@ -65,8 +65,8 @@ impl Content for ArrangerView<Tui> {
|
|||
}
|
||||
}
|
||||
|
||||
impl TransportViewState for ArrangerView<Tui> {
|
||||
fn focus (&self) -> TransportViewFocus {
|
||||
impl TransportViewState for ArrangerTui {
|
||||
fn focus (&self) -> TransportFocus {
|
||||
self.focus
|
||||
}
|
||||
fn focused (&self) -> bool {
|
||||
|
|
@ -102,7 +102,7 @@ fn track_widths (tracks: &[ArrangerTrack]) -> Vec<(usize, usize)> {
|
|||
}
|
||||
|
||||
pub fn arranger_content_vertical (
|
||||
view: &ArrangerView<Tui>,
|
||||
view: &ArrangerTui,
|
||||
factor: usize
|
||||
) -> impl Widget<Engine = Tui> + use<'_> {
|
||||
let timebase = view.timebase();
|
||||
|
|
@ -289,7 +289,7 @@ pub fn arranger_content_vertical (
|
|||
}
|
||||
|
||||
pub fn arranger_content_horizontal (
|
||||
view: &ArrangerView<Tui>,
|
||||
view: &ArrangerTui,
|
||||
) -> impl Widget<Engine = Tui> + use<'_> {
|
||||
let focused = view.focused;
|
||||
let _tracks = view.tracks();
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use crate::*;
|
||||
|
||||
/// Contains state for viewing and editing a phrase
|
||||
pub struct PhraseEditor<E: Engine> {
|
||||
pub struct PhraseTui<E: Engine> {
|
||||
_engine: PhantomData<E>,
|
||||
/// Phrase being played
|
||||
pub phrase: Option<Arc<RwLock<Phrase>>>,
|
||||
|
|
@ -31,7 +31,7 @@ pub struct PhraseEditor<E: Engine> {
|
|||
pub size: Measure<E>
|
||||
}
|
||||
|
||||
impl Widget for PhraseEditor<Tui> {
|
||||
impl Widget for PhraseTui<Tui> {
|
||||
type Engine = Tui;
|
||||
fn layout (&self, to: [u16;2]) -> Perhaps<[u16;2]> {
|
||||
PhraseView(&self, Default::default()).layout(to)
|
||||
|
|
@ -41,9 +41,9 @@ impl Widget for PhraseEditor<Tui> {
|
|||
}
|
||||
}
|
||||
|
||||
pub struct PhraseView<'a, T: PhraseEditorViewState>(pub &'a T);
|
||||
pub struct PhraseView<'a, T: PhraseTuiViewState>(pub &'a T);
|
||||
|
||||
pub trait PhraseEditorViewState: Send + Sync {
|
||||
pub trait PhraseTuiViewState: Send + Sync {
|
||||
fn focused (&self) -> bool;
|
||||
fn entered (&self) -> bool;
|
||||
fn keys (&self) -> &Buffer;
|
||||
|
|
@ -56,7 +56,7 @@ pub trait PhraseEditorViewState: Send + Sync {
|
|||
fn now (&self) -> &Arc<Pulse>;
|
||||
}
|
||||
|
||||
impl PhraseEditorViewState for PhraseEditor<Tui> {
|
||||
impl PhraseTuiViewState for PhraseTui<Tui> {
|
||||
fn focused (&self) -> bool {
|
||||
&self.focused
|
||||
}
|
||||
|
|
@ -89,7 +89,7 @@ impl PhraseEditorViewState for PhraseEditor<Tui> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, T: PhraseEditorViewState> Content for PhraseView<'a, T> {
|
||||
impl<'a, T: PhraseTuiViewState> Content for PhraseView<'a, T> {
|
||||
type Engine = Tui;
|
||||
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||
let phrase = self.0.phrase();
|
||||
|
|
@ -223,7 +223,7 @@ impl<'a, T: PhraseEditorViewState> Content for PhraseView<'a, T> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<E: Engine> PhraseEditor<E> {
|
||||
impl<E: Engine> PhraseTui<E> {
|
||||
pub fn new () -> Self {
|
||||
Self {
|
||||
_engine: Default::default(),
|
||||
|
|
@ -410,7 +410,7 @@ const NTH_OCTAVE: [&'static str; 11] = [
|
|||
];
|
||||
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
pub enum PhraseEditorCommand {
|
||||
pub enum PhraseCommand {
|
||||
// TODO: 1-9 seek markers that by default start every 8th of the phrase
|
||||
ToggleDirection,
|
||||
EnterEditMode,
|
||||
|
|
@ -425,15 +425,15 @@ pub enum PhraseEditorCommand {
|
|||
TimeZoomSet(usize),
|
||||
}
|
||||
|
||||
impl Handle<Tui> for PhraseEditor<Tui> {
|
||||
impl Handle<Tui> for PhraseTui<Tui> {
|
||||
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
|
||||
PhraseEditorCommand::execute_with_state(self, from)
|
||||
PhraseCommand::execute_with_state(self, from)
|
||||
}
|
||||
}
|
||||
|
||||
impl InputToCommand<Tui, PhraseEditor<Tui>> for PhraseEditorCommand {
|
||||
fn input_to_command (state: &PhraseEditor<Tui>, from: &TuiInput) -> Option<Self> {
|
||||
use PhraseEditorCommand::*;
|
||||
impl InputToCommand<Tui, PhraseTui<Tui>> for PhraseCommand {
|
||||
fn input_to_command (state: &PhraseTui<Tui>, from: &TuiInput) -> Option<Self> {
|
||||
use PhraseCommand::*;
|
||||
Some(match from.event() {
|
||||
key!(KeyCode::Char('`')) => ToggleDirection,
|
||||
key!(KeyCode::Enter) => EnterEditMode,
|
||||
|
|
@ -469,9 +469,9 @@ impl InputToCommand<Tui, PhraseEditor<Tui>> for PhraseEditorCommand {
|
|||
}
|
||||
}
|
||||
|
||||
impl<E: Engine> Command<PhraseEditor<E>> for PhraseEditorCommand {
|
||||
//fn translate (self, state: &PhraseEditor<E>) -> Self {
|
||||
//use PhraseEditorCommand::*;
|
||||
impl<E: Engine> Command<PhraseTui<E>> for PhraseCommand {
|
||||
//fn translate (self, state: &PhraseTui<E>) -> Self {
|
||||
//use PhraseCommand::*;
|
||||
//match self {
|
||||
//GoUp => match state.entered { true => NoteCursorInc, false => NoteScrollInc, },
|
||||
//GoDown => match state.entered { true => NoteCursorDec, false => NoteScrollDec, },
|
||||
|
|
@ -480,8 +480,8 @@ impl<E: Engine> Command<PhraseEditor<E>> for PhraseEditorCommand {
|
|||
//_ => self
|
||||
//}
|
||||
//}
|
||||
fn execute (self, state: &mut PhraseEditor<E>) -> Perhaps<Self> {
|
||||
use PhraseEditorCommand::*;
|
||||
fn execute (self, state: &mut PhraseTui<E>) -> Perhaps<Self> {
|
||||
use PhraseCommand::*;
|
||||
match self.translate(state) {
|
||||
ToggleDirection => {
|
||||
state.mode = !state.mode;
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ pub struct PhrasesTui {
|
|||
/// Scroll offset
|
||||
pub scroll: usize,
|
||||
/// Mode switch
|
||||
pub mode: Option<PhrasePoolMode>,
|
||||
pub mode: Option<PhrasesMode>,
|
||||
/// Whether this widget is focused
|
||||
pub focused: bool,
|
||||
/// Whether this widget is entered
|
||||
|
|
@ -16,7 +16,7 @@ pub struct PhrasesTui {
|
|||
}
|
||||
|
||||
/// Modes for phrase pool
|
||||
pub enum PhrasePoolMode {
|
||||
pub enum PhrasesMode {
|
||||
/// Renaming a pattern
|
||||
Rename(usize, String),
|
||||
/// Editing the length of a pattern
|
||||
|
|
@ -103,7 +103,7 @@ pub struct PhraseLength {
|
|||
pub focus: Option<PhraseLengthFocus>,
|
||||
}
|
||||
|
||||
impl<E: Engine> PhraseLength<E> {
|
||||
impl PhraseLength {
|
||||
pub fn new (pulses: usize, focus: Option<PhraseLengthFocus>) -> Self {
|
||||
Self { _engine: Default::default(), ppq: PPQ, bpb: 4, pulses, focus }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,12 +2,12 @@ use crate::*;
|
|||
|
||||
impl Handle<Tui> for PhrasePoolView<Tui> {
|
||||
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
|
||||
PhrasePoolViewCommand::execute_with_state(self, from)
|
||||
PhrasesCommand::execute_with_state(self, from)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
pub enum PhrasePoolViewCommand {
|
||||
pub enum PhrasesCommand {
|
||||
Select(usize),
|
||||
Edit(PhrasePoolCommand),
|
||||
Rename(PhraseRenameCommand),
|
||||
|
|
@ -33,9 +33,9 @@ pub enum PhraseLengthCommand {
|
|||
Cancel,
|
||||
}
|
||||
|
||||
impl InputToCommand<Tui, PhrasePoolView<Tui>> for PhrasePoolViewCommand {
|
||||
impl InputToCommand<Tui, PhrasePoolView<Tui>> for PhrasesCommand {
|
||||
fn input_to_command (state: &PhrasePoolView<Tui>, input: &TuiInput) -> Option<Self> {
|
||||
use PhrasePoolViewCommand as Cmd;
|
||||
use PhrasesCommand as Cmd;
|
||||
use PhrasePoolCommand as Edit;
|
||||
use PhraseRenameCommand as Rename;
|
||||
use PhraseLengthCommand as Length;
|
||||
|
|
@ -64,7 +64,7 @@ impl InputToCommand<Tui, PhrasePoolView<Tui>> for PhrasePoolViewCommand {
|
|||
}
|
||||
}
|
||||
|
||||
impl<E: Engine> Command<PhrasePoolView<E>> for PhrasePoolViewCommand {
|
||||
impl<E: Engine> Command<PhrasePoolView<E>> for PhrasesCommand {
|
||||
fn execute (self, view: &mut PhrasePoolView<E>) -> Perhaps<Self> {
|
||||
use PhraseRenameCommand as Rename;
|
||||
use PhraseLengthCommand as Length;
|
||||
|
|
|
|||
|
|
@ -1,46 +1,78 @@
|
|||
use crate::*;
|
||||
|
||||
impl Widget for TransportTui {
|
||||
impl Widget for PhrasesTui {
|
||||
type Engine = Tui;
|
||||
fn layout (&self, to: [u16;2]) -> Perhaps<[u16;2]> {
|
||||
TransportView(&self, Default::default()).layout(to)
|
||||
PhrasesView(&self).layout(to)
|
||||
}
|
||||
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
|
||||
TransportView(&self, Default::default()).render(to)
|
||||
PhrasesView(&self).render(to)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait PhrasesViewState {
|
||||
fn focused (&self) -> bool;
|
||||
fn entered (&self) -> bool;
|
||||
fn phrases (&self) -> Vec<()>;
|
||||
fn phrase (&self) -> ();
|
||||
fn mode (&self) -> PhrasesMode;
|
||||
}
|
||||
|
||||
impl PhrasesViewState for PhrasesTui {
|
||||
fn focused (&self) -> bool {
|
||||
todo!()
|
||||
}
|
||||
fn entered (&self) -> bool {
|
||||
todo!()
|
||||
}
|
||||
fn phrases (&self) -> Vec<()> {
|
||||
todo!()
|
||||
}
|
||||
fn phrase (&self) -> () {
|
||||
todo!()
|
||||
}
|
||||
fn mode (&self) -> PhrasesMode {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PhrasesView<'a, T: PhrasesViewState>(&'a T);
|
||||
|
||||
// TODO: Display phrases always in order of appearance
|
||||
impl Content for PhrasePoolView<Tui> {
|
||||
impl<'a, T: PhrasesViewState> Content for PhrasesView<'a, T> {
|
||||
type Engine = Tui;
|
||||
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||
let Self { focused, phrases, mode, .. } = self;
|
||||
let focused = self.focused();
|
||||
let entered = self.entered();
|
||||
let phrases = self.phrases();
|
||||
let phrase = self.phrase();
|
||||
let mode = self.mode();
|
||||
let content = col!(
|
||||
(i, phrase) in phrases.iter().enumerate() => Layers::new(|add|{
|
||||
let Phrase { ref name, color, length, .. } = *phrase.read().unwrap();
|
||||
let mut length = PhraseLength::new(length, None);
|
||||
if let Some(PhrasePoolMode::Length(phrase, new_length, focus)) = mode {
|
||||
if *focused && i == *phrase {
|
||||
length.pulses = *new_length;
|
||||
length.focus = Some(*focus);
|
||||
if let Some(PhrasesMode::Length(phrase, new_length, focus)) = mode {
|
||||
if *focused && i == phrase {
|
||||
length.pulses = new_length;
|
||||
length.focus = Some(focus);
|
||||
}
|
||||
}
|
||||
let length = length.align_e().fill_x();
|
||||
let row1 = lay!(format!(" {i}").align_w().fill_x(), length).fill_x();
|
||||
let mut row2 = format!(" {name}");
|
||||
if let Some(PhrasePoolMode::Rename(phrase, _)) = mode {
|
||||
if *focused && i == *phrase { row2 = format!("{row2}▄"); }
|
||||
if let Some(PhrasesMode::Rename(phrase, _)) = mode {
|
||||
if *focused && i == phrase { row2 = format!("{row2}▄"); }
|
||||
};
|
||||
let row2 = TuiStyle::bold(row2, true);
|
||||
add(&col!(row1, row2).fill_x().bg(color.base.rgb))?;
|
||||
Ok(if *focused && i == self.phrase { add(&CORNERS)?; })
|
||||
Ok(if *focused && i == phrase { add(&CORNERS)?; })
|
||||
})
|
||||
);
|
||||
let border_color = if *focused {Color::Rgb(100, 110, 40)} else {Color::Rgb(70, 80, 50)};
|
||||
let border = Lozenge(Style::default().bg(Color::Rgb(40, 50, 30)).fg(border_color));
|
||||
let content = content.fill_xy().bg(Color::Rgb(28, 35, 25)).border(border);
|
||||
let title_color = if *focused {Color::Rgb(150, 160, 90)} else {Color::Rgb(120, 130, 100)};
|
||||
let upper_left = format!("[{}] Phrases", if self.entered {"■"} else {" "});
|
||||
let upper_left = format!("[{}] Phrases", if entered {"■"} else {" "});
|
||||
let upper_right = format!("({})", phrases.len());
|
||||
lay!(
|
||||
content,
|
||||
|
|
|
|||
|
|
@ -3,14 +3,13 @@ use crate::*;
|
|||
impl TryFrom<&Arc<RwLock<JackClient>>> for SequencerTui {
|
||||
type Error = Box<dyn std::error::Error>;
|
||||
fn try_from (jack: &Arc<RwLock<JackClient>>) -> Usually<Self> {
|
||||
let clock = Arc::new(Clock::from(Instant::default()));
|
||||
Ok(Self::new(SequencerView {
|
||||
Ok(Self::new(SequencerTui {
|
||||
phrases: vec![],
|
||||
metronome: false,
|
||||
transport: jack.read().unwrap().transport(),
|
||||
jack: jack.clone(),
|
||||
focused: false,
|
||||
focus: TransportViewFocus::PlayPause,
|
||||
focus: TransportFocus::PlayPause,
|
||||
size: Measure::new(),
|
||||
phrases: vec![],
|
||||
editor: PhraseEditor::new(),
|
||||
|
|
@ -60,6 +59,8 @@ pub struct SequencerTui {
|
|||
/// Sections in the sequencer app that may be focused
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
|
||||
pub enum SequencerFocus {
|
||||
/// The menu bar is focused
|
||||
Menu,
|
||||
/// The transport (toolbar) is focused
|
||||
Transport,
|
||||
/// The phrase list (pool) is focused
|
||||
|
|
@ -114,7 +115,7 @@ impl Content for SequencerStatusBar {
|
|||
}
|
||||
|
||||
impl HasFocus for SequencerTui {
|
||||
type Item = AppViewFocus<SequencerFocus>;
|
||||
type Item = SequencerFocus;
|
||||
}
|
||||
|
||||
impl FocusEnter for SequencerTui {
|
||||
|
|
@ -141,7 +142,7 @@ impl FocusEnter for SequencerTui {
|
|||
}
|
||||
|
||||
impl FocusGrid for SequencerTui {
|
||||
type Item = AppViewFocus<SequencerFocus>;
|
||||
type Item = SequencerFocus;
|
||||
fn focus_cursor (&self) -> (usize, usize) {
|
||||
self.cursor
|
||||
}
|
||||
|
|
@ -149,12 +150,11 @@ impl FocusGrid for SequencerTui {
|
|||
&mut self.cursor
|
||||
}
|
||||
fn focus_layout (&self) -> &[&[Self::Item]] {
|
||||
use AppViewFocus::*;
|
||||
use SequencerFocus::*;
|
||||
&[
|
||||
&[Menu, Menu ],
|
||||
&[Content(Transport), Content(Transport) ],
|
||||
&[Content(PhrasePool), Content(PhraseEditor)],
|
||||
&[Transport, Transport ],
|
||||
&[PhrasePool, PhraseEditor],
|
||||
]
|
||||
}
|
||||
fn focus_update (&mut self) {
|
||||
|
|
@ -273,7 +273,7 @@ impl PlayheadApi for SequencerTui {
|
|||
impl PlayerApi for SequencerTui {}
|
||||
|
||||
impl TransportViewState for SequencerTui {
|
||||
fn focus (&self) -> TransportViewFocus {
|
||||
fn focus (&self) -> TransportFocus {
|
||||
self.focus
|
||||
}
|
||||
fn focused (&self) -> bool {
|
||||
|
|
|
|||
|
|
@ -14,13 +14,12 @@ pub enum SequencerCommand {
|
|||
Clear,
|
||||
Clock(ClockCommand),
|
||||
Playhead(PlayheadCommand),
|
||||
Phrases(PhrasePoolViewCommand),
|
||||
Editor(PhraseEditorCommand),
|
||||
Phrases(PhrasesCommand),
|
||||
Editor(PhraseCommand),
|
||||
}
|
||||
|
||||
impl InputToCommand<Tui, SequencerTui> for SequencerCommand {
|
||||
fn input_to_command (state: &SequencerTui, input: &TuiInput) -> Option<Self> {
|
||||
use AppViewFocus::*;
|
||||
use FocusCommand::*;
|
||||
use SequencerCommand::*;
|
||||
match input.event() {
|
||||
|
|
@ -33,35 +32,23 @@ impl InputToCommand<Tui, SequencerTui> for SequencerCommand {
|
|||
key!(KeyCode::Left) => Some(Self::Focus(Left)),
|
||||
key!(KeyCode::Right) => Some(Self::Focus(Right)),
|
||||
_ => Some(Self::App(match state.focused() {
|
||||
Content(SequencerFocus::Transport) =>
|
||||
TransportCommand::input_to_command(&state, input)
|
||||
.map(Transport),
|
||||
Content(SequencerFocus::PhrasePool) =>
|
||||
PhrasePoolViewCommand::input_to_command(&state.phrases, input)
|
||||
.map(Phrases),
|
||||
Content(SequencerFocus::PhraseEditor) =>
|
||||
PhraseEditorCommand::input_to_command(&state.editor, input)
|
||||
.map(Editor),
|
||||
SequencerFocus::Transport =>
|
||||
TransportCommand::input_to_command(&state, input).map(Transport),
|
||||
SequencerFocus::Phrases =>
|
||||
PhrasesCommand::input_to_command(&state.phrases, input).map(Phrases),
|
||||
SequencerFocus::PhraseEditor =>
|
||||
PhraseCommand::input_to_command(&state.editor, input).map(Editor),
|
||||
_ => return None,
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Command<SequencerTui> for SequencerCommand {
|
||||
fn execute (self, state: &mut SequencerTui) -> Perhaps<Self> {
|
||||
use AppViewCommand::*;
|
||||
match self {
|
||||
Focus(cmd) => delegate(cmd, Focus, state),
|
||||
App(cmd) => delegate(cmd, App, state),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Command<SequencerTui> for SequencerCommand {
|
||||
fn execute (self, state: &mut SequencerTui) -> Perhaps<Self> {
|
||||
use SequencerCommand::*;
|
||||
match self {
|
||||
Focus(cmd) => delegate(cmd, Focus, state),
|
||||
Phrases(cmd) => delegate(cmd, Phrases, &mut state.phrases),
|
||||
Editor(cmd) => delegate(cmd, Editor, &mut state.editor),
|
||||
Transport(cmd) => delegate(cmd, Transport, &mut state.transport)
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ pub struct TransportTui {
|
|||
transport: jack::Transport,
|
||||
/// Enable metronome?
|
||||
metronome: bool,
|
||||
focus: TransportViewFocus,
|
||||
focus: TransportFocus,
|
||||
focused: bool,
|
||||
size: Measure<E>,
|
||||
}
|
||||
|
|
@ -31,7 +31,7 @@ impl TryFrom<&Arc<RwLock<JackClient>>> for TransportTui {
|
|||
transport: jack.read().unwrap().transport(),
|
||||
jack: jack.clone(),
|
||||
focused: false,
|
||||
focus: TransportViewFocus::PlayPause,
|
||||
focus: TransportFocus::PlayPause,
|
||||
size: Measure::new(),
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use crate::*;
|
|||
/// Handle input.
|
||||
impl Handle<Tui> for TransportTui {
|
||||
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
|
||||
AppViewCommand::<TransportCommand>::execute_with_state(self, from)
|
||||
TransportCommand::execute_with_state(self, from)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -14,58 +14,46 @@ pub enum TransportCommand {
|
|||
Playhead(PlayheadCommand),
|
||||
}
|
||||
|
||||
impl InputToCommand<Tui, TransportTui> for AppViewCommand<TransportCommand> {
|
||||
fn input_to_command (app: &TransportTui, input: &TuiInput) -> Option<Self> {
|
||||
use KeyCode::{Left, Right};
|
||||
use FocusCommand::{Prev, Next};
|
||||
use TransportCommand::{Focus, Clock, Playhead};
|
||||
Some(match input.event() {
|
||||
key!(Left) => Focus(Prev),
|
||||
key!(Right) => Focus(Next),
|
||||
_ => TransportCommand::input_to_command(&app, input).map(App)?
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: TransportControl> InputToCommand<Tui, T> for TransportCommand {
|
||||
fn input_to_command (state: &T, input: &TuiInput) -> Option<Self> {
|
||||
use KeyCode::Char;
|
||||
use AppViewFocus::Content;
|
||||
use ClockCommand::{SetBpm, SetQuant, SetSync};
|
||||
use TransportViewFocus as Focus;
|
||||
use TransportCommand::{Clock, Playhead};
|
||||
use TransportFocus as Focused;
|
||||
use TransportCommand::{Focus, Clock, Playhead};
|
||||
let focused = state.focused();
|
||||
Some(match input.event() {
|
||||
key!(Left) => Focus(FocusCommand::Prev),
|
||||
key!(Right) => Focus(FocusCommand::Next),
|
||||
key!(Char('.')) => match focused {
|
||||
Content(Focus::Bpm) => Clock(SetBpm(state.bpm().get() + 1.0)),
|
||||
Content(Focus::Quant) => Clock(SetQuant(state.next_quant())),
|
||||
Content(Focus::Sync) => Clock(SetSync(state.next_sync())),
|
||||
Content(Focus::PlayPause) => Playhead(todo!()),
|
||||
Content(Focus::Clock) => Playhead(todo!()),
|
||||
Focused::Bpm => Clock(SetBpm(state.bpm().get() + 1.0)),
|
||||
Focused::Quant => Clock(SetQuant(state.next_quant())),
|
||||
Focused::Sync => Clock(SetSync(state.next_sync())),
|
||||
Focused::PlayPause => Playhead(todo!()),
|
||||
Focused::Clock => Playhead(todo!()),
|
||||
_ => {todo!()}
|
||||
},
|
||||
key!(KeyCode::Char(',')) => match focused {
|
||||
Content(Focus::Bpm) => Clock(SetBpm(state.bpm().get() - 1.0)),
|
||||
Content(Focus::Quant) => Clock(SetQuant(state.prev_quant())),
|
||||
Content(Focus::Sync) => Clock(SetSync(state.prev_sync())),
|
||||
Content(Focus::PlayPause) => Playhead(todo!()),
|
||||
Content(Focus::Clock) => Playhead(todo!()),
|
||||
Focused::Bpm => Clock(SetBpm(state.bpm().get() - 1.0)),
|
||||
Focused::Quant => Clock(SetQuant(state.prev_quant())),
|
||||
Focused::Sync => Clock(SetSync(state.prev_sync())),
|
||||
Focused::PlayPause => Playhead(todo!()),
|
||||
Focused::Clock => Playhead(todo!()),
|
||||
_ => {todo!()}
|
||||
},
|
||||
key!(KeyCode::Char('>')) => match focused {
|
||||
Content(Focus::Bpm) => Clock(SetBpm(state.bpm().get() + 0.001)),
|
||||
Content(Focus::Quant) => Clock(SetQuant(state.next_quant())),
|
||||
Content(Focus::Sync) => Clock(SetSync(state.next_sync())),
|
||||
Content(Focus::PlayPause) => Playhead(todo!()),
|
||||
Content(Focus::Clock) => Playhead(todo!()),
|
||||
Focused::Bpm => Clock(SetBpm(state.bpm().get() + 0.001)),
|
||||
Focused::Quant => Clock(SetQuant(state.next_quant())),
|
||||
Focused::Sync => Clock(SetSync(state.next_sync())),
|
||||
Focused::PlayPause => Playhead(todo!()),
|
||||
Focused::Clock => Playhead(todo!()),
|
||||
_ => {todo!()}
|
||||
},
|
||||
key!(KeyCode::Char('<')) => match focused {
|
||||
Content(Focus::Bpm) => Clock(SetBpm(state.bpm().get() - 0.001)),
|
||||
Content(Focus::Quant) => Clock(SetQuant(state.prev_quant())),
|
||||
Content(Focus::Sync) => Clock(SetSync(state.prev_sync())),
|
||||
Content(Focus::PlayPause) => Playhead(todo!()),
|
||||
Content(Focus::Clock) => Playhead(todo!()),
|
||||
Focused::Bpm => Clock(SetBpm(state.bpm().get() - 0.001)),
|
||||
Focused::Quant => Clock(SetQuant(state.prev_quant())),
|
||||
Focused::Sync => Clock(SetSync(state.prev_sync())),
|
||||
Focused::PlayPause => Playhead(todo!()),
|
||||
Focused::Clock => Playhead(todo!()),
|
||||
_ => {todo!()}
|
||||
},
|
||||
_ => return None
|
||||
|
|
@ -73,7 +61,7 @@ impl<T: TransportControl> InputToCommand<Tui, T> for TransportCommand {
|
|||
}
|
||||
}
|
||||
|
||||
pub trait TransportControl: FocusGrid<Item = AppViewFocus<TransportViewFocus>> {
|
||||
pub trait TransportControl: FocusGrid<Item = TransportFocus> {
|
||||
fn quant (&self) -> &Quantize;
|
||||
fn bpm (&self) -> &BeatsPerMinute;
|
||||
fn next_quant (&self) -> f64 {
|
||||
|
|
@ -103,31 +91,14 @@ impl TransportControl for TransportTui {
|
|||
}
|
||||
}
|
||||
|
||||
impl Command<TransportTui> for AppViewCommand<TransportCommand> {
|
||||
fn execute (self, state: &mut TransportTui) -> Perhaps<Self> {
|
||||
use AppViewCommand::{Focus, App};
|
||||
use FocusCommand::{Next, Prev};
|
||||
Ok(Some(match self {
|
||||
App(command) => if let Some(undo) = TransportCommand::execute(command, &mut state.app)? {
|
||||
App(undo)
|
||||
} else {
|
||||
return Ok(None)
|
||||
},
|
||||
Focus(command) => Focus(match command {
|
||||
Next => { todo!() },
|
||||
Prev => { todo!() },
|
||||
_ => { todo!() }
|
||||
}),
|
||||
_ => todo!()
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: TransportControl> Command<T> for TransportCommand {
|
||||
fn execute (self, state: &mut T) -> Perhaps<Self> {
|
||||
use TransportCommand::{Clock, Playhead};
|
||||
use TransportCommand::{Focus, Clock, Playhead};
|
||||
use ClockCommand::{SetBpm, SetQuant, SetSync};
|
||||
Ok(Some(match self {
|
||||
Focus(Next) => { todo!() }
|
||||
Focus(Prev) => { todo!() },
|
||||
Focus(_) => { unimplemented!() },
|
||||
Clock(SetBpm(bpm)) => Clock(SetBpm(state.bpm().set(bpm))),
|
||||
Clock(SetQuant(quant)) => Clock(SetQuant(state.quant().set(quant))),
|
||||
Clock(SetSync(sync)) => Clock(SetSync(state.sync().set(sync))),
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@ use crate::*;
|
|||
|
||||
/// Which item of the transport toolbar is focused
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum TransportViewFocus {
|
||||
pub enum TransportFocus {
|
||||
Menu,
|
||||
Bpm,
|
||||
Sync,
|
||||
PlayPause,
|
||||
|
|
@ -10,7 +11,7 @@ pub enum TransportViewFocus {
|
|||
Quant,
|
||||
}
|
||||
|
||||
impl TransportViewFocus {
|
||||
impl TransportFocus {
|
||||
pub fn next (&mut self) {
|
||||
*self = match self {
|
||||
Self::PlayPause => Self::Bpm,
|
||||
|
|
@ -40,7 +41,7 @@ impl TransportViewFocus {
|
|||
}
|
||||
|
||||
impl HasFocus for TransportTui {
|
||||
type Item = AppViewFocus<TransportViewFocus>;
|
||||
type Item = TransportFocus;
|
||||
}
|
||||
|
||||
impl FocusEnter for TransportTui {
|
||||
|
|
@ -60,7 +61,7 @@ impl FocusEnter for TransportTui {
|
|||
}
|
||||
|
||||
impl FocusGrid for TransportTui {
|
||||
type Item = AppViewFocus<TransportViewFocus>;
|
||||
type Item = TransportFocus;
|
||||
fn focus_cursor (&self) -> (usize, usize) {
|
||||
self.cursor
|
||||
}
|
||||
|
|
@ -68,17 +69,10 @@ impl FocusGrid for TransportTui {
|
|||
&mut self.cursor
|
||||
}
|
||||
fn focus_layout (&self) -> &[&[Self::Item]] {
|
||||
use AppViewFocus::*;
|
||||
use TransportViewFocus::*;
|
||||
use TransportFocus::*;
|
||||
&[
|
||||
&[Menu],
|
||||
&[
|
||||
Content(Bpm),
|
||||
Content(Sync),
|
||||
Content(PlayPause),
|
||||
Content(Clock),
|
||||
Content(Quant),
|
||||
],
|
||||
&[Bpm, Sync, PlayPause, Clock, Quant],
|
||||
]
|
||||
}
|
||||
fn focus_update (&mut self) {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ impl Widget for TransportTui {
|
|||
pub struct TransportView<'a, T: TransportViewState>(pub &'a T);
|
||||
|
||||
pub trait TransportViewState: Send + Sync {
|
||||
fn focus (&self) -> TransportViewFocus;
|
||||
fn focus (&self) -> TransportFocus;
|
||||
fn focused (&self) -> bool;
|
||||
fn transport_state (&self) -> Option<TransportState>;
|
||||
fn bpm_value (&self) -> f64;
|
||||
|
|
@ -23,7 +23,7 @@ pub trait TransportViewState: Send + Sync {
|
|||
}
|
||||
|
||||
impl TransportViewState for TransportTui {
|
||||
fn focus (&self) -> TransportViewFocus {
|
||||
fn focus (&self) -> TransportFocus {
|
||||
self.focus
|
||||
}
|
||||
fn focused (&self) -> bool {
|
||||
|
|
@ -51,7 +51,7 @@ impl<'a, T: TransportViewState> Content for TransportView<'a, T> {
|
|||
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||
let state = self.0;
|
||||
lay!(
|
||||
state.focus().wrap(state.focused(), TransportViewFocus::PlayPause, &Styled(
|
||||
state.focus().wrap(state.focused(), TransportFocus::PlayPause, &Styled(
|
||||
None,
|
||||
match state.transport_state() {
|
||||
Some(TransportState::Rolling) => "▶ PLAYING",
|
||||
|
|
@ -62,19 +62,19 @@ impl<'a, T: TransportViewState> Content for TransportView<'a, T> {
|
|||
).min_xy(11, 2).push_x(1)).align_x().fill_x(),
|
||||
|
||||
row!(
|
||||
state.focus().wrap(state.focused(), TransportViewFocus::Bpm, &Outset::X(1u16, {
|
||||
state.focus().wrap(state.focused(), TransportFocus::Bpm, &Outset::X(1u16, {
|
||||
let bpm = state.bpm_value();
|
||||
row! { "BPM ", format!("{}.{:03}", bpm as usize, (bpm * 1000.0) % 1000.0) }
|
||||
})),
|
||||
//let quant = state.focus().wrap(state.focused(), TransportViewFocus::Quant, &Outset::X(1u16, row! {
|
||||
//let quant = state.focus().wrap(state.focused(), TransportFocus::Quant, &Outset::X(1u16, row! {
|
||||
//"QUANT ", ppq_to_name(state.0.quant as usize)
|
||||
//})),
|
||||
state.focus().wrap(state.focused(), TransportViewFocus::Sync, &Outset::X(1u16, row! {
|
||||
state.focus().wrap(state.focused(), TransportFocus::Sync, &Outset::X(1u16, row! {
|
||||
"SYNC ", pulses_to_name(state.sync_value() as usize)
|
||||
}))
|
||||
).align_w().fill_x(),
|
||||
|
||||
state.focus().wrap(state.focused(), TransportViewFocus::Clock, &{
|
||||
state.focus().wrap(state.focused(), TransportFocus::Clock, &{
|
||||
let time1 = state.format_beat();
|
||||
let time2 = state.format_msu();
|
||||
row!("B" ,time1.as_str(), " T", time2.as_str()).outset_x(1)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue