Replace MH: IMusicHoard generic with a trait object #194

Merged
wojtek merged 1 commits from 192---replace-mh--imusichoard-generic-with-a-trait-object into main 2024-08-27 18:45:04 +02:00
10 changed files with 132 additions and 151 deletions

View File

@ -70,8 +70,10 @@ struct DbOpt {
no_database: bool,
}
fn with<Database: IDatabase, Library: ILibrary>(builder: MusicHoardBuilder<Database, Library>) {
let music_hoard = builder.build().expect("failed to initialise MusicHoard");
fn with<Database: IDatabase + 'static, Library: ILibrary + 'static>(
builder: MusicHoardBuilder<Database, Library>,
) {
let music_hoard = Box::new(builder.build().expect("failed to initialise MusicHoard"));
// Initialize the terminal user interface.
let backend = CrosstermBackend::new(io::stdout());
@ -92,7 +94,7 @@ fn with<Database: IDatabase, Library: ILibrary>(builder: MusicHoardBuilder<Datab
Tui::run(terminal, app, ui, handler, listener).expect("failed to run tui");
}
fn with_database<Library: ILibrary>(
fn with_database<Library: ILibrary + 'static>(
db_opt: DbOpt,
builder: MusicHoardBuilder<NoDatabase, Library>,
) {

View File

@ -2,19 +2,16 @@ use std::{thread, time};
use musichoard::collection::musicbrainz::IMusicBrainzRef;
use crate::tui::{
app::{
machine::{matches::AppMatchesInfo, App, AppInner, AppMachine},
selection::{Delta, ListSelection},
AppPublic, AppState, IAppInteractBrowse,
},
lib::IMusicHoard,
use crate::tui::app::{
machine::{matches::AppMatchesInfo, App, AppInner, AppMachine},
selection::{Delta, ListSelection},
AppPublic, AppState, IAppInteractBrowse,
};
pub struct AppBrowse;
impl<MH: IMusicHoard> AppMachine<MH, AppBrowse> {
pub fn browse(inner: AppInner<MH>) -> Self {
impl AppMachine<AppBrowse> {
pub fn browse(inner: AppInner) -> Self {
AppMachine {
inner,
state: AppBrowse,
@ -22,14 +19,14 @@ impl<MH: IMusicHoard> AppMachine<MH, AppBrowse> {
}
}
impl<MH: IMusicHoard> From<AppMachine<MH, AppBrowse>> for App<MH> {
fn from(machine: AppMachine<MH, AppBrowse>) -> Self {
impl From<AppMachine<AppBrowse>> for App {
fn from(machine: AppMachine<AppBrowse>) -> Self {
AppState::Browse(machine)
}
}
impl<'a, MH: IMusicHoard> From<&'a mut AppMachine<MH, AppBrowse>> for AppPublic<'a> {
fn from(machine: &'a mut AppMachine<MH, AppBrowse>) -> Self {
impl<'a> From<&'a mut AppMachine<AppBrowse>> for AppPublic<'a> {
fn from(machine: &'a mut AppMachine<AppBrowse>) -> Self {
AppPublic {
inner: (&mut machine.inner).into(),
state: AppState::Browse(()),
@ -37,8 +34,8 @@ impl<'a, MH: IMusicHoard> From<&'a mut AppMachine<MH, AppBrowse>> for AppPublic<
}
}
impl<MH: IMusicHoard> IAppInteractBrowse for AppMachine<MH, AppBrowse> {
type APP = App<MH>;
impl IAppInteractBrowse for AppMachine<AppBrowse> {
type APP = App;
fn quit(mut self) -> Self::APP {
self.inner.running = false;

View File

@ -1,17 +1,14 @@
use crate::tui::{
app::{
machine::{App, AppInner, AppMachine},
AppPublic, AppState, IAppInteractCritical,
},
lib::IMusicHoard,
use crate::tui::app::{
machine::{App, AppInner, AppMachine},
AppPublic, AppState, IAppInteractCritical,
};
pub struct AppCritical {
string: String,
}
impl<MH: IMusicHoard> AppMachine<MH, AppCritical> {
pub fn critical<S: Into<String>>(inner: AppInner<MH>, string: S) -> Self {
impl AppMachine<AppCritical> {
pub fn critical<S: Into<String>>(inner: AppInner, string: S) -> Self {
AppMachine {
inner,
state: AppCritical {
@ -21,14 +18,14 @@ impl<MH: IMusicHoard> AppMachine<MH, AppCritical> {
}
}
impl<MH: IMusicHoard> From<AppMachine<MH, AppCritical>> for App<MH> {
fn from(machine: AppMachine<MH, AppCritical>) -> Self {
impl From<AppMachine<AppCritical>> for App {
fn from(machine: AppMachine<AppCritical>) -> Self {
AppState::Critical(machine)
}
}
impl<'a, MH: IMusicHoard> From<&'a mut AppMachine<MH, AppCritical>> for AppPublic<'a> {
fn from(machine: &'a mut AppMachine<MH, AppCritical>) -> Self {
impl<'a> From<&'a mut AppMachine<AppCritical>> for AppPublic<'a> {
fn from(machine: &'a mut AppMachine<AppCritical>) -> Self {
AppPublic {
inner: (&mut machine.inner).into(),
state: AppState::Critical(&machine.state.string),
@ -36,8 +33,8 @@ impl<'a, MH: IMusicHoard> From<&'a mut AppMachine<MH, AppCritical>> for AppPubli
}
}
impl<MH: IMusicHoard> IAppInteractCritical for AppMachine<MH, AppCritical> {
type APP = App<MH>;
impl IAppInteractCritical for AppMachine<AppCritical> {
type APP = App;
fn no_op(self) -> Self::APP {
self.into()

View File

@ -1,17 +1,14 @@
use crate::tui::{
app::{
machine::{App, AppInner, AppMachine},
AppPublic, AppState, IAppInteractError,
},
lib::IMusicHoard,
use crate::tui::app::{
machine::{App, AppInner, AppMachine},
AppPublic, AppState, IAppInteractError,
};
pub struct AppError {
string: String,
}
impl<MH: IMusicHoard> AppMachine<MH, AppError> {
pub fn error<S: Into<String>>(inner: AppInner<MH>, string: S) -> Self {
impl AppMachine<AppError> {
pub fn error<S: Into<String>>(inner: AppInner, string: S) -> Self {
AppMachine {
inner,
state: AppError {
@ -21,14 +18,14 @@ impl<MH: IMusicHoard> AppMachine<MH, AppError> {
}
}
impl<MH: IMusicHoard> From<AppMachine<MH, AppError>> for App<MH> {
fn from(machine: AppMachine<MH, AppError>) -> Self {
impl From<AppMachine<AppError>> for App {
fn from(machine: AppMachine<AppError>) -> Self {
AppState::Error(machine)
}
}
impl<'a, MH: IMusicHoard> From<&'a mut AppMachine<MH, AppError>> for AppPublic<'a> {
fn from(machine: &'a mut AppMachine<MH, AppError>) -> Self {
impl<'a> From<&'a mut AppMachine<AppError>> for AppPublic<'a> {
fn from(machine: &'a mut AppMachine<AppError>) -> Self {
AppPublic {
inner: (&mut machine.inner).into(),
state: AppState::Error(&machine.state.string),
@ -36,8 +33,8 @@ impl<'a, MH: IMusicHoard> From<&'a mut AppMachine<MH, AppError>> for AppPublic<'
}
}
impl<MH: IMusicHoard> IAppInteractError for AppMachine<MH, AppError> {
type APP = App<MH>;
impl IAppInteractError for AppMachine<AppError> {
type APP = App;
fn dismiss_error(self) -> Self::APP {
AppMachine::browse(self.inner).into()

View File

@ -1,15 +1,12 @@
use crate::tui::{
app::{
machine::{App, AppInner, AppMachine},
AppPublic, AppState, IAppInteractInfo,
},
lib::IMusicHoard,
use crate::tui::app::{
machine::{App, AppInner, AppMachine},
AppPublic, AppState, IAppInteractInfo,
};
pub struct AppInfo;
impl<MH: IMusicHoard> AppMachine<MH, AppInfo> {
pub fn info(inner: AppInner<MH>) -> Self {
impl AppMachine<AppInfo> {
pub fn info(inner: AppInner) -> Self {
AppMachine {
inner,
state: AppInfo,
@ -17,14 +14,14 @@ impl<MH: IMusicHoard> AppMachine<MH, AppInfo> {
}
}
impl<MH: IMusicHoard> From<AppMachine<MH, AppInfo>> for App<MH> {
fn from(machine: AppMachine<MH, AppInfo>) -> Self {
impl From<AppMachine<AppInfo>> for App {
fn from(machine: AppMachine<AppInfo>) -> Self {
AppState::Info(machine)
}
}
impl<'a, MH: IMusicHoard> From<&'a mut AppMachine<MH, AppInfo>> for AppPublic<'a> {
fn from(machine: &'a mut AppMachine<MH, AppInfo>) -> Self {
impl<'a> From<&'a mut AppMachine<AppInfo>> for AppPublic<'a> {
fn from(machine: &'a mut AppMachine<AppInfo>) -> Self {
AppPublic {
inner: (&mut machine.inner).into(),
state: AppState::Info(()),
@ -32,8 +29,8 @@ impl<'a, MH: IMusicHoard> From<&'a mut AppMachine<MH, AppInfo>> for AppPublic<'a
}
}
impl<MH: IMusicHoard> IAppInteractInfo for AppMachine<MH, AppInfo> {
type APP = App<MH>;
impl IAppInteractInfo for AppMachine<AppInfo> {
type APP = App;
fn hide_info_overlay(self) -> Self::APP {
AppMachine::browse(self.inner).into()

View File

@ -2,12 +2,9 @@ use std::cmp;
use musichoard::{collection::album::Album, interface::musicbrainz::Match};
use crate::tui::{
app::{
machine::{App, AppInner, AppMachine},
AppPublic, AppPublicMatches, AppState, IAppInteractMatches, WidgetState,
},
lib::IMusicHoard,
use crate::tui::app::{
machine::{App, AppInner, AppMachine},
AppPublic, AppPublicMatches, AppState, IAppInteractMatches, WidgetState,
};
#[derive(Clone, Debug, PartialEq, Eq)]
@ -22,8 +19,8 @@ pub struct AppMatches {
state: WidgetState,
}
impl<MH: IMusicHoard> AppMachine<MH, AppMatches> {
pub fn matches(inner: AppInner<MH>, matches_info_vec: Vec<AppMatchesInfo>) -> Self {
impl AppMachine<AppMatches> {
pub fn matches(inner: AppInner, matches_info_vec: Vec<AppMatchesInfo>) -> Self {
let mut index = None;
let mut state = WidgetState::default();
if let Some(matches_info) = matches_info_vec.first() {
@ -44,14 +41,14 @@ impl<MH: IMusicHoard> AppMachine<MH, AppMatches> {
}
}
impl<MH: IMusicHoard> From<AppMachine<MH, AppMatches>> for App<MH> {
fn from(machine: AppMachine<MH, AppMatches>) -> Self {
impl From<AppMachine<AppMatches>> for App {
fn from(machine: AppMachine<AppMatches>) -> Self {
AppState::Matches(machine)
}
}
impl<'a, MH: IMusicHoard> From<&'a mut AppMachine<MH, AppMatches>> for AppPublic<'a> {
fn from(machine: &'a mut AppMachine<MH, AppMatches>) -> Self {
impl<'a> From<&'a mut AppMachine<AppMatches>> for AppPublic<'a> {
fn from(machine: &'a mut AppMachine<AppMatches>) -> Self {
let (matching, matches) = match machine.state.index {
Some(index) => (
Some(&machine.state.matches_info_vec[index].matching),
@ -71,8 +68,8 @@ impl<'a, MH: IMusicHoard> From<&'a mut AppMachine<MH, AppMatches>> for AppPublic
}
}
impl<MH: IMusicHoard> IAppInteractMatches for AppMachine<MH, AppMatches> {
type APP = App<MH>;
impl IAppInteractMatches for AppMachine<AppMatches> {
type APP = App;
fn prev_match(mut self) -> Self::APP {
if let Some(list_index) = self.state.state.list.selected() {

View File

@ -19,30 +19,30 @@ use matches::AppMatches;
use reload::AppReload;
use search::AppSearch;
pub type App<MH> = AppState<
AppMachine<MH, AppBrowse>,
AppMachine<MH, AppInfo>,
AppMachine<MH, AppReload>,
AppMachine<MH, AppSearch>,
AppMachine<MH, AppMatches>,
AppMachine<MH, AppError>,
AppMachine<MH, AppCritical>,
pub type App = AppState<
AppMachine<AppBrowse>,
AppMachine<AppInfo>,
AppMachine<AppReload>,
AppMachine<AppSearch>,
AppMachine<AppMatches>,
AppMachine<AppError>,
AppMachine<AppCritical>,
>;
pub struct AppMachine<MH: IMusicHoard, STATE> {
inner: AppInner<MH>,
pub struct AppMachine<STATE> {
inner: AppInner,
state: STATE,
}
pub struct AppInner<MH: IMusicHoard> {
pub struct AppInner {
running: bool,
music_hoard: MH,
music_hoard: Box<dyn IMusicHoard>,
mb_api: Box<dyn IMusicBrainz>,
selection: Selection,
}
impl<MH: IMusicHoard> App<MH> {
pub fn new(mut music_hoard: MH, mb_api: Box<dyn IMusicBrainz>) -> Self {
impl App {
pub fn new(mut music_hoard: Box<dyn IMusicHoard>, mb_api: Box<dyn IMusicBrainz>) -> Self {
let init_result = Self::init(&mut music_hoard);
let inner = AppInner::new(music_hoard, mb_api);
match init_result {
@ -51,12 +51,12 @@ impl<MH: IMusicHoard> App<MH> {
}
}
fn init(music_hoard: &mut MH) -> Result<(), musichoard::Error> {
fn init(music_hoard: &mut Box<dyn IMusicHoard>) -> Result<(), musichoard::Error> {
music_hoard.rescan_library()?;
Ok(())
}
fn inner_ref(&self) -> &AppInner<MH> {
fn inner_ref(&self) -> &AppInner {
match self {
AppState::Browse(browse) => &browse.inner,
AppState::Info(info) => &info.inner,
@ -68,7 +68,7 @@ impl<MH: IMusicHoard> App<MH> {
}
}
fn inner_mut(&mut self) -> &mut AppInner<MH> {
fn inner_mut(&mut self) -> &mut AppInner {
match self {
AppState::Browse(browse) => &mut browse.inner,
AppState::Info(info) => &mut info.inner,
@ -81,14 +81,14 @@ impl<MH: IMusicHoard> App<MH> {
}
}
impl<MH: IMusicHoard> IAppInteract for App<MH> {
type BS = AppMachine<MH, AppBrowse>;
type IS = AppMachine<MH, AppInfo>;
type RS = AppMachine<MH, AppReload>;
type SS = AppMachine<MH, AppSearch>;
type MS = AppMachine<MH, AppMatches>;
type ES = AppMachine<MH, AppError>;
type CS = AppMachine<MH, AppCritical>;
impl IAppInteract for App {
type BS = AppMachine<AppBrowse>;
type IS = AppMachine<AppInfo>;
type RS = AppMachine<AppReload>;
type SS = AppMachine<AppSearch>;
type MS = AppMachine<AppMatches>;
type ES = AppMachine<AppError>;
type CS = AppMachine<AppCritical>;
fn is_running(&self) -> bool {
self.inner_ref().running
@ -106,7 +106,7 @@ impl<MH: IMusicHoard> IAppInteract for App<MH> {
}
}
impl<MH: IMusicHoard> IAppAccess for App<MH> {
impl IAppAccess for App {
fn get(&mut self) -> AppPublic {
match self {
AppState::Browse(browse) => browse.into(),
@ -120,8 +120,8 @@ impl<MH: IMusicHoard> IAppAccess for App<MH> {
}
}
impl<MH: IMusicHoard> AppInner<MH> {
pub fn new(music_hoard: MH, mb_api: Box<dyn IMusicBrainz>) -> Self {
impl AppInner {
pub fn new(music_hoard: Box<dyn IMusicHoard>, mb_api: Box<dyn IMusicBrainz>) -> Self {
let selection = Selection::new(music_hoard.get_collection());
AppInner {
running: true,
@ -132,8 +132,8 @@ impl<MH: IMusicHoard> AppInner<MH> {
}
}
impl<'a, MH: IMusicHoard> From<&'a mut AppInner<MH>> for AppPublicInner<'a> {
fn from(inner: &'a mut AppInner<MH>) -> Self {
impl<'a> From<&'a mut AppInner> for AppPublicInner<'a> {
fn from(inner: &'a mut AppInner) -> Self {
AppPublicInner {
collection: inner.music_hoard.get_collection(),
selection: &mut inner.selection,
@ -203,14 +203,14 @@ mod tests {
}
}
pub fn music_hoard(collection: Collection) -> MockIMusicHoard {
let mut music_hoard = MockIMusicHoard::new();
pub fn music_hoard(collection: Collection) -> Box<MockIMusicHoard> {
let mut music_hoard = Box::new(MockIMusicHoard::new());
music_hoard.expect_get_collection().return_const(collection);
music_hoard
}
fn music_hoard_init(collection: Collection) -> MockIMusicHoard {
fn music_hoard_init(collection: Collection) -> Box<MockIMusicHoard> {
let mut music_hoard = music_hoard(collection);
music_hoard
@ -225,14 +225,14 @@ mod tests {
Box::new(MockIMusicBrainz::new())
}
pub fn inner(music_hoard: MockIMusicHoard) -> AppInner<MockIMusicHoard> {
pub fn inner(music_hoard: Box<MockIMusicHoard>) -> AppInner {
AppInner::new(music_hoard, mb_api())
}
pub fn inner_with_mb(
music_hoard: MockIMusicHoard,
music_hoard: Box<MockIMusicHoard>,
mb_api: Box<MockIMusicBrainz>,
) -> AppInner<MockIMusicHoard> {
) -> AppInner {
AppInner::new(music_hoard, mb_api)
}
@ -362,7 +362,7 @@ mod tests {
#[test]
fn init_error() {
let mut music_hoard = MockIMusicHoard::new();
let mut music_hoard = Box::new(MockIMusicHoard::new());
music_hoard
.expect_rescan_library()

View File

@ -1,16 +1,13 @@
use crate::tui::{
app::{
machine::{App, AppInner, AppMachine},
selection::KeySelection,
AppPublic, AppState, IAppInteractReload,
},
lib::IMusicHoard,
use crate::tui::app::{
machine::{App, AppInner, AppMachine},
selection::KeySelection,
AppPublic, AppState, IAppInteractReload,
};
pub struct AppReload;
impl<MH: IMusicHoard> AppMachine<MH, AppReload> {
pub fn reload(inner: AppInner<MH>) -> Self {
impl AppMachine<AppReload> {
pub fn reload(inner: AppInner) -> Self {
AppMachine {
inner,
state: AppReload,
@ -18,13 +15,13 @@ impl<MH: IMusicHoard> AppMachine<MH, AppReload> {
}
}
impl<MH: IMusicHoard> From<AppMachine<MH, AppReload>> for App<MH> {
fn from(machine: AppMachine<MH, AppReload>) -> Self {
impl From<AppMachine<AppReload>> for App {
fn from(machine: AppMachine<AppReload>) -> Self {
AppState::Reload(machine)
}
}
impl<'a, MH: IMusicHoard> From<&'a mut AppMachine<MH, AppReload>> for AppPublic<'a> {
fn from(machine: &'a mut AppMachine<MH, AppReload>) -> Self {
impl<'a> From<&'a mut AppMachine<AppReload>> for AppPublic<'a> {
fn from(machine: &'a mut AppMachine<AppReload>) -> Self {
AppPublic {
inner: (&mut machine.inner).into(),
state: AppState::Reload(()),
@ -32,8 +29,8 @@ impl<'a, MH: IMusicHoard> From<&'a mut AppMachine<MH, AppReload>> for AppPublic<
}
}
impl<MH: IMusicHoard> IAppInteractReload for AppMachine<MH, AppReload> {
type APP = App<MH>;
impl IAppInteractReload for AppMachine<AppReload> {
type APP = App;
fn reload_library(mut self) -> Self::APP {
let previous = KeySelection::get(
@ -62,12 +59,12 @@ impl<MH: IMusicHoard> IAppInteractReload for AppMachine<MH, AppReload> {
}
}
trait IAppInteractReloadPrivate<MH: IMusicHoard> {
fn refresh(self, previous: KeySelection, result: Result<(), musichoard::Error>) -> App<MH>;
trait IAppInteractReloadPrivate {
fn refresh(self, previous: KeySelection, result: Result<(), musichoard::Error>) -> App;
}
impl<MH: IMusicHoard> IAppInteractReloadPrivate<MH> for AppMachine<MH, AppReload> {
fn refresh(mut self, previous: KeySelection, result: Result<(), musichoard::Error>) -> App<MH> {
impl IAppInteractReloadPrivate for AppMachine<AppReload> {
fn refresh(mut self, previous: KeySelection, result: Result<(), musichoard::Error>) -> App {
match result {
Ok(()) => {
self.inner

View File

@ -3,13 +3,10 @@ use once_cell::sync::Lazy;
use musichoard::collection::{album::Album, artist::Artist, track::Track};
use crate::tui::{
app::{
machine::{App, AppInner, AppMachine},
selection::{ListSelection, SelectionState},
AppPublic, AppState, Category, IAppInteractSearch,
},
lib::IMusicHoard,
use crate::tui::app::{
machine::{App, AppInner, AppMachine},
selection::{ListSelection, SelectionState},
AppPublic, AppState, Category, IAppInteractSearch,
};
// Unlikely that this covers all possible strings, but it should at least cover strings
@ -34,8 +31,8 @@ struct AppSearchMemo {
char: bool,
}
impl<MH: IMusicHoard> AppMachine<MH, AppSearch> {
pub fn search(inner: AppInner<MH>, orig: ListSelection) -> Self {
impl AppMachine<AppSearch> {
pub fn search(inner: AppInner, orig: ListSelection) -> Self {
AppMachine {
inner,
state: AppSearch {
@ -47,14 +44,14 @@ impl<MH: IMusicHoard> AppMachine<MH, AppSearch> {
}
}
impl<MH: IMusicHoard> From<AppMachine<MH, AppSearch>> for App<MH> {
fn from(machine: AppMachine<MH, AppSearch>) -> Self {
impl From<AppMachine<AppSearch>> for App {
fn from(machine: AppMachine<AppSearch>) -> Self {
AppState::Search(machine)
}
}
impl<'a, MH: IMusicHoard> From<&'a mut AppMachine<MH, AppSearch>> for AppPublic<'a> {
fn from(machine: &'a mut AppMachine<MH, AppSearch>) -> Self {
impl<'a> From<&'a mut AppMachine<AppSearch>> for AppPublic<'a> {
fn from(machine: &'a mut AppMachine<AppSearch>) -> Self {
AppPublic {
inner: (&mut machine.inner).into(),
state: AppState::Search(&machine.state.string),
@ -62,8 +59,8 @@ impl<'a, MH: IMusicHoard> From<&'a mut AppMachine<MH, AppSearch>> for AppPublic<
}
}
impl<MH: IMusicHoard> IAppInteractSearch for AppMachine<MH, AppSearch> {
type APP = App<MH>;
impl IAppInteractSearch for AppMachine<AppSearch> {
type APP = App;
fn append_character(mut self, ch: char) -> Self::APP {
self.state.string.push(ch);
@ -127,7 +124,7 @@ trait IAppInteractSearchPrivate {
fn normalize_search(search: &str, lowercase: bool, asciify: bool) -> String;
}
impl<MH: IMusicHoard> IAppInteractSearchPrivate for AppMachine<MH, AppSearch> {
impl IAppInteractSearchPrivate for AppMachine<AppSearch> {
fn incremental_search(&mut self, next: bool) {
let collection = self.inner.music_hoard.get_collection();
let search = &self.state.string;

View File

@ -191,8 +191,8 @@ mod tests {
Terminal::new(backend).unwrap()
}
fn music_hoard(collection: Collection) -> MockIMusicHoard {
let mut music_hoard = MockIMusicHoard::new();
fn music_hoard(collection: Collection) -> Box<MockIMusicHoard> {
let mut music_hoard = Box::new(MockIMusicHoard::new());
music_hoard.expect_reload_database().returning(|| Ok(()));
music_hoard.expect_rescan_library().returning(|| Ok(()));
@ -201,7 +201,7 @@ mod tests {
music_hoard
}
fn app(collection: Collection) -> App<MockIMusicHoard> {
fn app(collection: Collection) -> App {
App::new(music_hoard(collection), Box::new(MockIMusicBrainz::new()))
}
@ -216,11 +216,11 @@ mod tests {
listener
}
fn handler() -> MockIEventHandler<App<MockIMusicHoard>> {
fn handler() -> MockIEventHandler<App> {
let mut handler = MockIEventHandler::new();
handler
.expect_handle_next_event()
.return_once(|app: App<MockIMusicHoard>| Ok(app.force_quit()));
.return_once(|app: App| Ok(app.force_quit()));
handler
}