remove HasPhraseList; 8470LOC

This commit is contained in:
🪞👃🪞 2024-12-18 13:46:07 +01:00
parent efda18293d
commit 623fce73a4
7 changed files with 133 additions and 233 deletions

View file

@ -1,7 +1,6 @@
use crate::*; use crate::*;
use crate::api::ArrangerTrackCommand; use crate::api::ArrangerTrackCommand;
use crate::api::ArrangerSceneCommand; use crate::api::ArrangerSceneCommand;
/// Root view for standalone `tek_arranger` /// Root view for standalone `tek_arranger`
pub struct ArrangerTui { pub struct ArrangerTui {
pub jack: Arc<RwLock<JackClient>>, pub jack: Arc<RwLock<JackClient>>,
@ -55,10 +54,8 @@ has_editor!(|self:ArrangerTui|self.editor);
handle!(<Tui>|self: ArrangerTui, input|ArrangerCommand::execute_with_state(self, input)); handle!(<Tui>|self: ArrangerTui, input|ArrangerCommand::execute_with_state(self, input));
render!(<Tui>|self: ArrangerTui|{ render!(<Tui>|self: ArrangerTui|{
let arranger_focused = self.arranger_focused(); let arranger_focused = self.arranger_focused();
let transport_focused = if let ArrangerFocus::Transport(_) = self.focus.inner() { let transport_focused = match self.focus.inner() {
true ArrangerFocus::Transport(_) => true, _ => false
} else {
false
}; };
let transport = TransportView::from((self, None, transport_focused)); let transport = TransportView::from((self, None, transport_focused));
let with_transport = move|x|col!([transport, x]); let with_transport = move|x|col!([transport, x]);
@ -72,20 +69,8 @@ render!(<Tui>|self: ArrangerTui|{
}; };
add(&self.size) add(&self.size)
}))); })));
with_transport(col!([ let with_pool = |x|Split::right(false, self.splits[1], PhraseListView(&self.phrases), x);
Fixed::h(self.splits[0], lay!([ with_transport(col!([Fixed::h(self.splits[0], arranger()), with_pool(&self.editor),]))
arranger(),
Tui::push_x(1, Tui::fg(
TuiTheme::title_fg(arranger_focused),
format!("[{}] Arranger", if self.entered {
""
} else {
" "
})
))
])),
Split::right(false, self.splits[1], PhraseListView(&self.phrases), &self.editor),
]))
}); });
audio!(|self: ArrangerTui, client, scope|{ audio!(|self: ArrangerTui, client, scope|{
// Start profiling cycle // Start profiling cycle
@ -124,23 +109,7 @@ audio!(|self: ArrangerTui, client, scope|{
self.perf.update(t0, scope); self.perf.update(t0, scope);
return Control::Continue return Control::Continue
}); });
#[derive(Clone, Debug)] pub enum ArrangerCommand {
impl HasPhraseList for ArrangerTui {
fn phrases_focused (&self) -> bool {
self.focused() == ArrangerFocus::Phrases
}
fn phrases_entered (&self) -> bool {
self.entered() && self.phrases_focused()
}
fn phrases_mode (&self) -> &Option<PhraseListMode> {
&self.phrases.mode
}
fn phrase_index (&self) -> usize {
self.phrases.phrase.load(Ordering::Relaxed)
}
}
#[derive(Clone, Debug)]
pub enum ArrangerCommand {
Focus(FocusCommand<ArrangerFocus>), Focus(FocusCommand<ArrangerFocus>),
Undo, Undo,
Redo, Redo,
@ -384,8 +353,6 @@ impl TransportControl<ArrangerFocus> for ArrangerTui {
} }
} }
} }
has_clock!(|self:ArrangerTrack|self.player.clock());
has_player!(|self:ArrangerTrack|self.player);
/// Sections in the arranger app that may be focused /// Sections in the arranger app that may be focused
#[derive(Copy, Clone, PartialEq, Eq, Debug)] #[derive(Copy, Clone, PartialEq, Eq, Debug)]
@ -809,8 +776,7 @@ impl HasScenes<ArrangerScene> for ArrangerTui {
} }
} }
#[derive(Default, Debug, Clone)] #[derive(Default, Debug, Clone)] pub struct ArrangerScene {
pub struct ArrangerScene {
/// Name of scene /// Name of scene
pub(crate) name: Arc<RwLock<String>>, pub(crate) name: Arc<RwLock<String>>,
/// Clips in scene, one per track /// Clips in scene, one per track
@ -818,7 +784,6 @@ pub struct ArrangerScene {
/// Identifying color of scene /// Identifying color of scene
pub(crate) color: ItemColor, pub(crate) color: ItemColor,
} }
impl ArrangerSceneApi for ArrangerScene { impl ArrangerSceneApi for ArrangerScene {
fn name (&self) -> &Arc<RwLock<String>> { fn name (&self) -> &Arc<RwLock<String>> {
&self.name &self.name
@ -830,7 +795,6 @@ impl ArrangerSceneApi for ArrangerScene {
self.color self.color
} }
} }
impl HasTracks<ArrangerTrack> for ArrangerTui { impl HasTracks<ArrangerTrack> for ArrangerTui {
fn tracks (&self) -> &Vec<ArrangerTrack> { fn tracks (&self) -> &Vec<ArrangerTrack> {
&self.tracks &self.tracks
@ -839,7 +803,6 @@ impl HasTracks<ArrangerTrack> for ArrangerTui {
&mut self.tracks &mut self.tracks
} }
} }
impl ArrangerTracksApi<ArrangerTrack> for ArrangerTui { impl ArrangerTracksApi<ArrangerTrack> for ArrangerTui {
fn track_add (&mut self, name: Option<&str>, color: Option<ItemColor>) fn track_add (&mut self, name: Option<&str>, color: Option<ItemColor>)
-> Usually<&mut ArrangerTrack> -> Usually<&mut ArrangerTrack>
@ -863,8 +826,7 @@ impl ArrangerTracksApi<ArrangerTrack> for ArrangerTui {
} }
} }
#[derive(Debug)] #[derive(Debug)] pub struct ArrangerTrack {
pub struct ArrangerTrack {
/// Name of track /// Name of track
pub(crate) name: Arc<RwLock<String>>, pub(crate) name: Arc<RwLock<String>>,
/// Preferred width of track column /// Preferred width of track column
@ -874,7 +836,8 @@ pub struct ArrangerTrack {
/// MIDI player state /// MIDI player state
pub(crate) player: PhrasePlayerModel, pub(crate) player: PhrasePlayerModel,
} }
has_clock!(|self:ArrangerTrack|self.player.clock());
has_player!(|self:ArrangerTrack|self.player);
impl ArrangerTrackApi for ArrangerTrack { impl ArrangerTrackApi for ArrangerTrack {
/// Name of track /// Name of track
fn name (&self) -> &Arc<RwLock<String>> { fn name (&self) -> &Arc<RwLock<String>> {
@ -893,7 +856,6 @@ impl ArrangerTrackApi for ArrangerTrack {
self.color self.color
} }
} }
#[derive(PartialEq, Clone, Copy, Debug)] #[derive(PartialEq, Clone, Copy, Debug)]
/// Represents the current user selection in the arranger /// Represents the current user selection in the arranger
pub enum ArrangerSelection { pub enum ArrangerSelection {

View file

@ -33,10 +33,10 @@ pub enum GrooveboxCommand {
handle!(<Tui>|self:GrooveboxTui,input|GrooveboxCommand::execute_with_state(self, input)); handle!(<Tui>|self:GrooveboxTui,input|GrooveboxCommand::execute_with_state(self, input));
input_to_command!(GrooveboxCommand: <Tui>|state:GrooveboxTui,input|match input.event() { input_to_command!(GrooveboxCommand: <Tui>|state:GrooveboxTui,input|match input.event() {
_ => match state.focus { _ => match state.focus {
GrooveboxFocus::Sequencer => GrooveboxFocus::Sequencer => GrooveboxCommand::Sequencer(
GrooveboxCommand::Sequencer(SequencerCommand::input_to_command(&state.sequencer, input)?), SequencerCommand::input_to_command(&state.sequencer, input)?),
GrooveboxFocus::Sampler => GrooveboxFocus::Sampler => GrooveboxCommand::Sampler(
GrooveboxCommand::Sampler(SamplerCommand::input_to_command(&state.sampler, input)?), SamplerCommand::input_to_command(&state.sampler, input)?),
} }
}); });
command!(|self:GrooveboxCommand,state:GrooveboxTui|match self { command!(|self:GrooveboxCommand,state:GrooveboxTui|match self {

View file

@ -54,7 +54,9 @@ pub enum SamplerCommand {
NoteOff(u7) NoteOff(u7)
} }
input_to_command!(SamplerCommand:<Tui>|state:SamplerTui,input|match state.mode { input_to_command!(SamplerCommand:<Tui>|state:SamplerTui,input|match state.mode {
Some(SamplerMode::Import(..)) => Self::Import(FileBrowserCommand::input_to_command(state, input)?), Some(SamplerMode::Import(..)) => Self::Import(
FileBrowserCommand::input_to_command(state, input)?
),
_ => match input.event() { _ => match input.event() {
// load sample // load sample
key_pat!(Char('l')) => Self::Import(FileBrowserCommand::Begin), key_pat!(Char('l')) => Self::Import(FileBrowserCommand::Begin),

View file

@ -76,20 +76,6 @@ audio!(|self:SequencerTui, client, scope|{
self.perf.update(t0, scope); self.perf.update(t0, scope);
Control::Continue Control::Continue
}); });
impl HasPhraseList for SequencerTui {
fn phrases_focused (&self) -> bool {
true
}
fn phrases_entered (&self) -> bool {
true
}
fn phrases_mode (&self) -> &Option<PhraseListMode> {
&self.phrases.mode
}
fn phrase_index (&self) -> usize {
self.phrases.phrase.load(Ordering::Relaxed)
}
}
has_size!(<Tui>|self:SequencerTui|&self.size); has_size!(<Tui>|self:SequencerTui|&self.size);
has_clock!(|self:SequencerTui|&self.clock); has_clock!(|self:SequencerTui|&self.clock);
has_phrases!(|self:SequencerTui|self.phrases.phrases); has_phrases!(|self:SequencerTui|self.phrases.phrases);

View file

@ -2,7 +2,6 @@ use crate::*;
use KeyCode::{Up, Down, Right, Left, Enter, Esc, Char, Backspace}; use KeyCode::{Up, Down, Right, Left, Enter, Esc, Char, Backspace};
use FileBrowserCommand::*; use FileBrowserCommand::*;
use super::phrase_list::PhraseListMode::{Import, Export}; use super::phrase_list::PhraseListMode::{Import, Export};
/// Browses for phrase to import/export /// Browses for phrase to import/export
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct FileBrowser { pub struct FileBrowser {
@ -14,7 +13,16 @@ pub struct FileBrowser {
pub scroll: usize, pub scroll: usize,
pub size: Measure<Tui> pub size: Measure<Tui>
} }
/// Commands supported by [FileBrowser]
#[derive(Debug, Clone, PartialEq)]
pub enum FileBrowserCommand {
Begin,
Cancel,
Confirm,
Select(usize),
Chdir(PathBuf),
Filter(String),
}
render!(<Tui>|self: FileBrowser|{ render!(<Tui>|self: FileBrowser|{
Stack::down(|add|{ Stack::down(|add|{
let mut i = 0; let mut i = 0;
@ -34,7 +42,6 @@ render!(<Tui>|self: FileBrowser|{
Ok(()) Ok(())
}) })
}); });
impl FileBrowser { impl FileBrowser {
pub fn new (cwd: Option<PathBuf>) -> Usually<Self> { pub fn new (cwd: Option<PathBuf>) -> Usually<Self> {
let cwd = if let Some(cwd) = cwd { cwd } else { std::env::current_dir()? }; let cwd = if let Some(cwd) = cwd { cwd } else { std::env::current_dir()? };
@ -83,106 +90,3 @@ impl FileBrowser {
Self::new(Some(self.path())) Self::new(Some(self.path()))
} }
} }
/// Commands supported by [FileBrowser]
#[derive(Debug, Clone, PartialEq)]
pub enum FileBrowserCommand {
Begin,
Cancel,
Confirm,
Select(usize),
Chdir(PathBuf),
Filter(String),
}
command!(|self: FileBrowserCommand, state: PhraseListModel|{
let mode = state.phrases_mode_mut();
match mode {
Some(Import(index, ref mut browser)) => match self {
Cancel => {
*mode = None;
},
Chdir(cwd) => {
*mode = Some(Import(*index, FileBrowser::new(Some(cwd))?));
},
Select(index) => {
browser.index = index;
},
Confirm => {
if browser.is_file() {
let index = *index;
let path = browser.path();
*mode = None;
PhrasePoolCommand::Import(index, path).execute(state)?;
} else if browser.is_dir() {
*mode = Some(Import(*index, browser.chdir()?));
}
},
_ => todo!(),
},
Some(PhraseListMode::Export(index, ref mut browser)) => match self {
Cancel => {
*mode = None;
},
Chdir(cwd) => {
*mode = Some(PhraseListMode::Export(*index, FileBrowser::new(Some(cwd))?));
},
Select(index) => {
browser.index = index;
},
_ => unreachable!()
},
_ => unreachable!(),
};
None
});
input_to_command!(FileBrowserCommand:<Tui>|state:PhraseListModel,from|{
if let Some(PhraseListMode::Import(_index, browser)) = state.phrases_mode() {
match from.event() {
key_pat!(Up) => Select(
browser.index.overflowing_sub(1).0.min(browser.len().saturating_sub(1))
),
key_pat!(Down) => Select(
browser.index.saturating_add(1) % browser.len()
),
key_pat!(Right) => Chdir(browser.cwd.clone()),
key_pat!(Left) => Chdir(browser.cwd.clone()),
key_pat!(Enter) => Confirm,
key_pat!(Char(_)) => { todo!() },
key_pat!(Backspace) => { todo!() },
key_pat!(Esc) => Self::Cancel,
_ => return None
}
} else if let Some(PhraseListMode::Export(_index, browser)) = state.phrases_mode() {
match from.event() {
key_pat!(Up) => Select(browser.index.overflowing_sub(1).0.min(browser.len())),
key_pat!(Down) => Select(browser.index.saturating_add(1) % browser.len()),
key_pat!(Right) => Chdir(browser.cwd.clone()),
key_pat!(Left) => Chdir(browser.cwd.clone()),
key_pat!(Enter) => Confirm,
key_pat!(Char(_)) => { todo!() },
key_pat!(Backspace) => { todo!() },
key_pat!(Esc) => Self::Cancel,
_ => return None
}
} else {
unreachable!()
}
});
input_to_command!(PhraseLengthCommand:<Tui>|state:PhraseListModel,from|{
if let Some(PhraseListMode::Length(_, length, _)) = state.phrases_mode() {
match from.event() {
key_pat!(Up) => Self::Inc,
key_pat!(Down) => Self::Dec,
key_pat!(Right) => Self::Next,
key_pat!(Left) => Self::Prev,
key_pat!(Enter) => Self::Set(*length),
key_pat!(Esc) => Self::Cancel,
_ => return None
}
} else {
unreachable!()
}
});

View file

@ -2,7 +2,6 @@ use crate::*;
use super::phrase_list::{PhraseListModel, PhraseListMode}; use super::phrase_list::{PhraseListModel, PhraseListMode};
use PhraseLengthFocus::*; use PhraseLengthFocus::*;
use PhraseLengthCommand::*; use PhraseLengthCommand::*;
/// Displays and edits phrase length. /// Displays and edits phrase length.
#[derive(Clone)] #[derive(Clone)]
pub struct PhraseLength { pub struct PhraseLength {
@ -15,7 +14,6 @@ pub struct PhraseLength {
/// Selected subdivision /// Selected subdivision
pub focus: Option<PhraseLengthFocus>, pub focus: Option<PhraseLengthFocus>,
} }
impl PhraseLength { impl PhraseLength {
pub fn new (pulses: usize, focus: Option<PhraseLengthFocus>) -> Self { pub fn new (pulses: usize, focus: Option<PhraseLengthFocus>) -> Self {
Self { ppq: PPQ, bpb: 4, pulses, focus } Self { ppq: PPQ, bpb: 4, pulses, focus }
@ -39,7 +37,6 @@ impl PhraseLength {
format!("{:>02}", self.ticks()) format!("{:>02}", self.ticks())
} }
} }
/// Focused field of `PhraseLength` /// Focused field of `PhraseLength`
#[derive(Copy, Clone, Debug)] #[derive(Copy, Clone, Debug)]
pub enum PhraseLengthFocus { pub enum PhraseLengthFocus {
@ -50,7 +47,6 @@ pub enum PhraseLengthFocus {
/// Editing the number of ticks /// Editing the number of ticks
Tick, Tick,
} }
impl PhraseLengthFocus { impl PhraseLengthFocus {
pub fn next (&mut self) { pub fn next (&mut self) {
*self = match self { *self = match self {
@ -67,7 +63,6 @@ impl PhraseLengthFocus {
} }
} }
} }
render!(<Tui>|self: PhraseLength|{ render!(<Tui>|self: PhraseLength|{
let bars = ||self.bars_string(); let bars = ||self.bars_string();
let beats = ||self.beats_string(); let beats = ||self.beats_string();
@ -83,7 +78,6 @@ render!(<Tui>|self: PhraseLength|{
add(&row!([" ", bars(), ".", beats(), "[", ticks()])), add(&row!([" ", bars(), ".", beats(), "[", ticks()])),
}) })
}); });
#[derive(Copy, Clone, Debug, PartialEq)] #[derive(Copy, Clone, Debug, PartialEq)]
pub enum PhraseLengthCommand { pub enum PhraseLengthCommand {
Begin, Begin,
@ -94,9 +88,7 @@ pub enum PhraseLengthCommand {
Inc, Inc,
Dec, Dec,
} }
command!(|self:PhraseLengthCommand,state:PhraseListModel|{
impl Command<PhraseListModel> for PhraseLengthCommand {
fn execute (self, state: &mut PhraseListModel) -> Perhaps<Self> {
match state.phrases_mode_mut().clone() { match state.phrases_mode_mut().clone() {
Some(PhraseListMode::Length(phrase, ref mut length, ref mut focus)) => match self { Some(PhraseListMode::Length(phrase, ref mut length, ref mut focus)) => match self {
Cancel => { *state.phrases_mode_mut() = None; }, Cancel => { *state.phrases_mode_mut() = None; },
@ -124,6 +116,20 @@ impl Command<PhraseListModel> for PhraseLengthCommand {
}, },
_ => unreachable!() _ => unreachable!()
}; };
Ok(None) None
});
input_to_command!(PhraseLengthCommand:<Tui>|state:PhraseListModel,from|{
if let Some(PhraseListMode::Length(_, length, _)) = state.phrases_mode() {
match from.event() {
key_pat!(Up) => Self::Inc,
key_pat!(Down) => Self::Dec,
key_pat!(Right) => Self::Next,
key_pat!(Left) => Self::Prev,
key_pat!(Enter) => Self::Set(*length),
key_pat!(Esc) => Self::Cancel,
_ => return None
} }
} else {
unreachable!()
} }
});

View file

@ -156,7 +156,6 @@ fn to_phrases_command (state: &PhraseListModel, input: &TuiInput) -> Option<Phra
_ => return None _ => return None
}) })
} }
impl Default for PhraseListModel { impl Default for PhraseListModel {
fn default () -> Self { fn default () -> Self {
Self { Self {
@ -168,7 +167,6 @@ impl Default for PhraseListModel {
} }
} }
} }
impl From<&Arc<RwLock<Phrase>>> for PhraseListModel { impl From<&Arc<RwLock<Phrase>>> for PhraseListModel {
fn from (phrase: &Arc<RwLock<Phrase>>) -> Self { fn from (phrase: &Arc<RwLock<Phrase>>) -> Self {
let mut model = Self::default(); let mut model = Self::default();
@ -177,10 +175,8 @@ impl From<&Arc<RwLock<Phrase>>> for PhraseListModel {
model model
} }
} }
has_phrases!(|self:PhraseListModel|self.phrases); has_phrases!(|self:PhraseListModel|self.phrases);
has_phrase!(|self:PhraseListModel|self.phrases[self.phrase_index()]); has_phrase!(|self:PhraseListModel|self.phrases[self.phrase_index()]);
impl PhraseListModel { impl PhraseListModel {
pub(crate) fn phrase_index (&self) -> usize { pub(crate) fn phrase_index (&self) -> usize {
self.phrase.load(Relaxed) self.phrase.load(Relaxed)
@ -195,16 +191,7 @@ impl PhraseListModel {
&mut self.mode &mut self.mode
} }
} }
pub trait HasPhraseList: HasPhrases {
fn phrases_focused (&self) -> bool;
fn phrases_entered (&self) -> bool;
fn phrases_mode (&self) -> &Option<PhraseListMode>;
fn phrase_index (&self) -> usize;
}
pub struct PhraseListView<'a>(pub(crate) &'a PhraseListModel); pub struct PhraseListView<'a>(pub(crate) &'a PhraseListModel);
// TODO: Display phrases always in order of appearance // TODO: Display phrases always in order of appearance
render!(<Tui>|self: PhraseListView<'a>|{ render!(<Tui>|self: PhraseListView<'a>|{
let PhraseListModel { phrases, mode, .. } = self.0; let PhraseListModel { phrases, mode, .. } = self.0;
@ -254,14 +241,12 @@ render!(<Tui>|self: PhraseListView<'a>|{
add(&self.0.size) add(&self.0.size)
})) }))
}); });
pub struct PhraseSelector { pub struct PhraseSelector {
pub(crate) title: &'static str, pub(crate) title: &'static str,
pub(crate) name: String, pub(crate) name: String,
pub(crate) color: ItemPalette, pub(crate) color: ItemPalette,
pub(crate) time: String, pub(crate) time: String,
} }
// TODO: Display phrases always in order of appearance // TODO: Display phrases always in order of appearance
render!(<Tui>|self: PhraseSelector|Fixed::wh(24, 1, row!([ render!(<Tui>|self: PhraseSelector|Fixed::wh(24, 1, row!([
Tui::fg(self.color.lightest.rgb, Tui::bold(true, &self.title)), Tui::fg(self.color.lightest.rgb, Tui::bold(true, &self.title)),
@ -270,7 +255,6 @@ render!(<Tui>|self: PhraseSelector|Fixed::wh(24, 1, row!([
Tui::bg(self.color.dark.rgb, &self.time), Tui::bg(self.color.dark.rgb, &self.time),
])), ])),
]))); ])));
impl PhraseSelector { impl PhraseSelector {
// beats elapsed // beats elapsed
pub fn play_phrase <T: HasPlayPhrase + HasClock> (state: &T) -> Self { pub fn play_phrase <T: HasPlayPhrase + HasClock> (state: &T) -> Self {
@ -314,13 +298,69 @@ impl PhraseSelector {
}; };
Self { title: " Next|", time, name, color, } Self { title: " Next|", time, name, color, }
} }
pub fn edit_phrase (phrase: &Option<Arc<RwLock<Phrase>>>) -> Self { }
let (time, name, color) = if let Some(phrase) = phrase { command!(|self: FileBrowserCommand, state: PhraseListModel|{
let phrase = phrase.read().unwrap(); use PhraseListMode::*;
(format!("{}", phrase.length), phrase.name.clone(), phrase.color) use FileBrowserCommand::*;
} else { let mode = &mut state.mode;
("".to_string(), " ".to_string(), ItemPalette::from(TuiTheme::g(64))) match mode {
Some(Import(index, ref mut browser)) => match self {
Cancel => { *mode = None; },
Chdir(cwd) => { *mode = Some(Import(*index, FileBrowser::new(Some(cwd))?)); },
Select(index) => { browser.index = index; },
Confirm => if browser.is_file() {
let index = *index;
let path = browser.path();
*mode = None;
PhrasePoolCommand::Import(index, path).execute(state)?;
} else if browser.is_dir() {
*mode = Some(Import(*index, browser.chdir()?));
},
_ => todo!(),
},
Some(Export(index, ref mut browser)) => match self {
Cancel => { *mode = None; },
Chdir(cwd) => { *mode = Some(Export(*index, FileBrowser::new(Some(cwd))?)); },
Select(index) => { browser.index = index; },
_ => unreachable!()
},
_ => unreachable!(),
}; };
Self { title: "Editing:", time, name, color } None
});
input_to_command!(FileBrowserCommand:<Tui>|state:PhraseListModel,from|{
use PhraseListMode::*;
use FileBrowserCommand::*;
use KeyCode::{Up, Down, Left, Right, Enter, Esc, Backspace, Char};
if let Some(PhraseListMode::Import(_index, browser)) = &state.mode {
match from.event() {
key_pat!(Up) => Select(browser.index.overflowing_sub(1).0
.min(browser.len().saturating_sub(1))),
key_pat!(Down) => Select(browser.index.saturating_add(1)
% browser.len()),
key_pat!(Right) => Chdir(browser.cwd.clone()),
key_pat!(Left) => Chdir(browser.cwd.clone()),
key_pat!(Enter) => Confirm,
key_pat!(Char(_)) => { todo!() },
key_pat!(Backspace) => { todo!() },
key_pat!(Esc) => Cancel,
_ => return None
} }
} else if let Some(PhraseListMode::Export(_index, browser)) = &state.mode {
match from.event() {
key_pat!(Up) => Select(browser.index.overflowing_sub(1).0
.min(browser.len())),
key_pat!(Down) => Select(browser.index.saturating_add(1)
% browser.len()),
key_pat!(Right) => Chdir(browser.cwd.clone()),
key_pat!(Left) => Chdir(browser.cwd.clone()),
key_pat!(Enter) => Confirm,
key_pat!(Char(_)) => { todo!() },
key_pat!(Backspace) => { todo!() },
key_pat!(Esc) => Cancel,
_ => return None
} }
} else {
unreachable!()
}
});