diff --git a/config/config_arranger.edn b/config/config_arranger.edn index 8ac29f05..247b5c6c 100644 --- a/config/config_arranger.edn +++ b/config/config_arranger.edn @@ -3,7 +3,7 @@ (info "A session grid.") (keys - (layer-if :is-editing "./keys_editor.edn") + (layer-if :focus-editor "./keys_editor.edn") (layer-if :focus-message "./keys_message.edn") (layer-if :focus-device-add "./keys_device_add.edn") (layer-if :focus-browser "./keys_browser.edn") @@ -26,6 +26,6 @@ (bsp/s :view-tracks-devices (bsp/s :view-tracks-outputs (bsp/s :view-tracks-names - (fill/xy (either :is-editing + (fill/xy (either :focus-editor (bsp/e :view-scenes-names :view-editor) :view-scenes))))))))))) diff --git a/config/keys_arranger.edn b/config/keys_arranger.edn index 76e1e6b4..a6411816 100644 --- a/config/keys_arranger.edn +++ b/config/keys_arranger.edn @@ -1,7 +1,5 @@ (@c color) (@q launch) -(@t select :select-track-header) -(@s select :select-scene-header) (@tab project edit) (@enter project edit) (@escape project home) @@ -15,3 +13,5 @@ (@down select :select-scene-next) (@left select :select-track-prev) (@right select :select-track-next) +(@t select :select-track) +(@s select :select-scene) diff --git a/config/keys_arranger_clip.edn b/config/keys_arranger_clip.edn new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/config/keys_arranger_clip.edn @@ -0,0 +1 @@ + diff --git a/config/keys_arranger_device.edn b/config/keys_arranger_device.edn new file mode 100644 index 00000000..e69de29b diff --git a/config/keys_arranger_input.rs b/config/keys_arranger_input.rs new file mode 100644 index 00000000..e69de29b diff --git a/config/keys_arranger_output.rs b/config/keys_arranger_output.rs new file mode 100644 index 00000000..e69de29b diff --git a/config/keys_arranger_scene.edn b/config/keys_arranger_scene.edn new file mode 100644 index 00000000..e69de29b diff --git a/config/keys_arranger_track.rs b/config/keys_arranger_track.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/app/src/api.rs b/crates/app/src/api.rs index e15634e1..71b88e0a 100644 --- a/crates/app/src/api.rs +++ b/crates/app/src/api.rs @@ -1,27 +1,19 @@ use crate::*; use std::path::PathBuf; - type MaybeClip = Option>>; - macro_rules! ns { ($C:ty, $s:expr, $a:expr, $W:expr) => { <$C>::try_from_expr($s, $a).map($W) } } macro_rules! cmd { ($cmd:expr) => {{ $cmd; None }}; } macro_rules! cmd_todo { ($msg:literal) => {{ println!($msg); None }}; } - handle!(TuiIn: |self: App, input|Ok(if let Some(command) = self.config.keys.command(self, input) { - let undo = command.execute(self)?; - if let Some(undo) = undo { - self.history.push(undo); - } + // FIXME failed commands not persisted in undo history + let undo = command.clone().execute(self)?; + self.history.push((command, undo)); Some(true) } else { None })); - -#[tengri_proc::command(App)] impl AppCommand { - fn dialog (app: &mut App, dialog: Option) -> Perhaps { - app.toggle_dialog(dialog); - Ok(None) - } +#[tengri_proc::command(App)] +impl AppCommand { fn cancel_dialog (app: &mut App) -> Perhaps { app.toggle_dialog(None); Ok(None) @@ -73,11 +65,8 @@ handle!(TuiIn: |self: App, input|Ok(if let Some(command) = self.config.keys.comm app.tracks_stop_all(); Ok(None) } - fn sampler (app: &mut App, command: SamplerCommand) -> Perhaps { - Ok(app.project.sampler_mut() - .map(|s|command.delegate(s, |command|Self::Sampler{command})) - .transpose()? - .flatten()) + fn dialog (app: &mut App, command: DialogCommand) -> Perhaps { + Ok(command.delegate(&mut app.dialog, |command|Self::Dialog{command})?) } fn project (app: &mut App, command: ArrangementCommand) -> Perhaps { Ok(command.delegate(&mut app.project, |command|Self::Project{command})?) @@ -85,9 +74,6 @@ handle!(TuiIn: |self: App, input|Ok(if let Some(command) = self.config.keys.comm fn clock (app: &mut App, command: ClockCommand) -> Perhaps { Ok(command.execute(app.clock_mut())?.map(|command|Self::Clock{command})) } - fn message (app: &mut App, command: MessageCommand) -> Perhaps { - Ok(command.delegate(app, |command|Self::Message{command})?) - } fn editor (app: &mut App, command: MidiEditCommand) -> Perhaps { Ok(if let Some(editor) = app.editor_mut() { let undo = command.clone().delegate(editor, |command|AppCommand::Editor{command})?; @@ -102,6 +88,12 @@ handle!(TuiIn: |self: App, input|Ok(if let Some(command) = self.config.keys.comm None }) } + fn sampler (app: &mut App, command: SamplerCommand) -> Perhaps { + Ok(app.project.sampler_mut() + .map(|s|command.delegate(s, |command|Self::Sampler{command})) + .transpose()? + .flatten()) + } fn pool (app: &mut App, command: PoolCommand) -> Perhaps { let undo = command.clone().delegate( &mut app.pool, @@ -121,40 +113,33 @@ handle!(TuiIn: |self: App, input|Ok(if let Some(command) = self.config.keys.comm Ok(undo) } } - impl<'state> Context<'state, ClockCommand> for App { fn get <'source> (&'state self, iter: &mut TokenIter<'source>) -> Option { Context::get(&self.clock(), iter) } } - impl<'state> Context<'state, MidiEditCommand> for App { fn get <'source> (&'state self, iter: &mut TokenIter<'source>) -> Option { self.editor().map(|e|Context::get(e, iter)).flatten() } } - impl<'state> Context<'state, PoolCommand> for App { fn get <'source> (&'state self, iter: &mut TokenIter<'source>) -> Option { Context::get(&self.pool, iter) } } - impl<'state> Context<'state, SamplerCommand> for App { fn get <'source> (&'state self, iter: &mut TokenIter<'source>) -> Option { self.project.sampler().map(|p|Context::get(p, iter)).flatten() } } - impl<'state> Context<'state, ArrangementCommand> for App { fn get <'source> (&'state self, iter: &mut TokenIter<'source>) -> Option { Context::get(&self.project, iter) } } - -#[tengri_proc::command(App)] impl MessageCommand { - fn dismiss (app: &mut App) -> Perhaps { - app.dialog = None; - Ok(None) +impl<'state> Context<'state, DialogCommand> for App { + fn get <'source> (&'state self, iter: &mut TokenIter<'source>) -> Option { + Context::get(&self, iter) } } diff --git a/crates/app/src/model.rs b/crates/app/src/model.rs index 5553da0e..feffe3bf 100644 --- a/crates/app/src/model.rs +++ b/crates/app/src/model.rs @@ -18,7 +18,7 @@ pub struct App { /// Contains the currently edited musical arrangement pub project: Arrangement, /// Undo history - pub history: Vec, + pub history: Vec<(AppCommand, Option)>, // Dialog overlay pub dialog: Option, // Cache of formatted strings @@ -179,9 +179,6 @@ impl App { fn focus_editor (&self) -> bool { self.project.editor.is_some() } - fn is_editing (&self) -> bool { - self.project.editor.is_some() - } fn focus_message (&self) -> bool { matches!(self.dialog, Some(Dialog::Message(..))) } @@ -192,16 +189,20 @@ impl App { self.browser().is_some() } fn focus_clip (&self) -> bool { - !self.is_editing() && self.selection().is_clip() + !self.focus_editor() && matches!(self.selection(), + Selection::TrackClip{..}) } fn focus_track (&self) -> bool { - !self.is_editing() && self.selection().is_track() + !self.focus_editor() && matches!(self.selection(), + Selection::Track(..)) } fn focus_scene (&self) -> bool { - !self.is_editing() && self.selection().is_scene() + !self.focus_editor() && matches!(self.selection(), + Selection::Scene(..)) } fn focus_mix (&self) -> bool { - !self.is_editing() && self.selection().is_mix() + !self.focus_editor() && matches!(self.selection(), + Selection::Mix) } fn focus_pool_import (&self) -> bool { matches!(self.pool.mode, Some(PoolMode::Import(..))) @@ -260,29 +261,32 @@ impl App { fn scene_count (&self) -> usize { self.scenes().len() } - fn scene_selection (&self) -> Option { + fn scene_selected (&self) -> Option { self.selection().scene() } fn track_count (&self) -> usize { self.tracks().len() } - fn track_selection (&self) -> Option { + fn track_selected (&self) -> Option { self.selection().track() } + fn select_scene (&self) -> Selection { + self.selection().select_scene(self.tracks().len()) + } fn select_scene_next (&self) -> Selection { - self.selection().scene_next(self.scenes().len()) + self.selection().select_scene_next(self.scenes().len()) } fn select_scene_prev (&self) -> Selection { - self.selection().scene_prev() + self.selection().select_scene_prev() } - fn select_track_header (&self) -> Selection { - self.selection().track_header(self.tracks().len()) + fn select_track (&self) -> Selection { + self.selection().select_track(self.tracks().len()) } fn select_track_next (&self) -> Selection { - self.selection().track_next(self.tracks().len()) + self.selection().select_track_next(self.tracks().len()) } fn select_track_prev (&self) -> Selection { - self.selection().track_prev() + self.selection().select_track_prev() } fn clip_selected (&self) -> Option>> { match self.selection() { diff --git a/crates/app/src/view.rs b/crates/app/src/view.rs index dc93ffce..c3c94321 100644 --- a/crates/app/src/view.rs +++ b/crates/app/src/view.rs @@ -42,6 +42,7 @@ impl App { self.tracks(), self.scenes() ))))); + add(&FieldV(theme, "History", Fill::x(Align::w(format!("{}", self.history.len()))))); } })) } diff --git a/crates/device/src/arranger/arranger_select.rs b/crates/device/src/arranger/arranger_select.rs index db9536ba..665acb42 100644 --- a/crates/device/src/arranger/arranger_select.rs +++ b/crates/device/src/arranger/arranger_select.rs @@ -39,18 +39,6 @@ pub enum Selection { /// Focus identification methods impl Selection { - pub fn is_mix (&self) -> bool { - matches!(self, Self::Mix) - } - pub fn is_track (&self) -> bool { - matches!(self, Self::Track(_)) - } - pub fn is_scene (&self) -> bool { - matches!(self, Self::Scene(_)) - } - pub fn is_clip (&self) -> bool { - matches!(self, Self::TrackClip {..}) - } pub fn track (&self) -> Option { use Selection::*; match self { @@ -62,7 +50,7 @@ impl Selection { _ => None } } - pub fn track_header (&self, track_count: usize) -> Self { + pub fn select_track (&self, track_count: usize) -> Self { use Selection::*; match self { Mix => Track(0), @@ -72,7 +60,7 @@ impl Selection { _ => todo!(), } } - pub fn track_next (&self, len: usize) -> Self { + pub fn select_track_next (&self, len: usize) -> Self { use Selection::*; match self { Mix => Track(0), @@ -90,7 +78,7 @@ impl Selection { _ => todo!() } } - pub fn track_prev (&self) -> Self { + pub fn select_track_prev (&self) -> Self { use Selection::*; match self { Mix => Mix, @@ -109,7 +97,16 @@ impl Selection { _ => None } } - pub fn scene_next (&self, len: usize) -> Self { + pub fn select_scene (&self, scene_count: usize) -> Self { + use Selection::*; + match self { + Mix | Track(_) => Scene(0), + Scene(s) => Scene((s + 1) % scene_count), + TrackClip { scene, .. } => Track(*scene), + _ => todo!(), + } + } + pub fn select_scene_next (&self, len: usize) -> Self { use Selection::*; match self { Mix => Scene(0), @@ -127,7 +124,7 @@ impl Selection { _ => todo!() } } - pub fn scene_prev (&self) -> Self { + pub fn select_scene_prev (&self) -> Self { use Selection::*; match self { Mix | Scene(0) => Mix, @@ -142,11 +139,9 @@ impl Selection { use Selection::*; format!("{}", match self { Mix => "Everything".to_string(), - Scene(s) => scenes.get(*s) - .map(|scene|format!("S{s}: {}", &scene.name)) + Scene(s) => scenes.get(*s).map(|scene|format!("S{s}: {}", &scene.name)) .unwrap_or_else(||"S??".into()), - Track(t) => tracks.get(*t) - .map(|track|format!("T{t}: {}", &track.name)) + Track(t) => tracks.get(*t).map(|track|format!("T{t}: {}", &track.name)) .unwrap_or_else(||"T??".into()), TrackClip { track, scene } => match (tracks.get(*track), scenes.get(*scene)) { (Some(_), Some(s)) => match s.clip(*track) { @@ -159,6 +154,3 @@ impl Selection { }).into() } } - -impl Arrangement { -} diff --git a/crates/device/src/arranger/arranger_view.rs b/crates/device/src/arranger/arranger_view.rs index c9ebc98a..5e7bd877 100644 --- a/crates/device/src/arranger/arranger_view.rs +++ b/crates/device/src/arranger/arranger_view.rs @@ -90,8 +90,11 @@ impl Arrangement { for (index, track, x1, x2) in self.tracks_with_sizes() { add(&Fixed::xy(self.track_width(index, track), h + 1, Tui::bg(track.color.dark.rgb, Align::nw(Map::south(2, move||0..h, - |_, index|Fixed::xy(track.width as u16, 2, Tui::bg(ItemTheme::G[32].base.rgb, - Align::nw(format!(" · {}", "--"))))))))); + |_, index|Fixed::xy(track.width as u16, 2, + Tui::fg_bg( + ItemTheme::G[32].lightest.rgb, + ItemTheme::G[32].dark.rgb, + Align::nw(format!(" · {}", "--"))))))))); } })) } diff --git a/crates/device/src/dialog.rs b/crates/device/src/dialog.rs index f9f7c309..430edafc 100644 --- a/crates/device/src/dialog.rs +++ b/crates/device/src/dialog.rs @@ -1,4 +1,6 @@ use crate::*; +mod dialog_api; pub use self::dialog_api::*; +mod dialog_view; pub use self::dialog_view::*; /// Various possible dialog overlays #[derive(Clone, Debug)] @@ -16,98 +18,3 @@ pub enum Dialog { pub enum Message { FailedToAddDevice, } - -content!(TuiOut: |self: Message| match self { - Self::FailedToAddDevice => "Failed to add device." -}); - -content!(TuiOut: |self: Dialog| match self { - Self::Menu(_) => - self.view_dialog_menu().boxed(), - Self::Help(offset) => - self.view_dialog_help(*offset).boxed(), - Self::Browser(target, browser) => - self.view_dialog_browser(target, browser).boxed(), - Self::Options => - self.view_dialog_options().boxed(), - Self::Device(index) => - self.view_dialog_device(*index).boxed(), - Self::Message(message) => - self.view_dialog_message(message).boxed(), -}); - -impl Dialog { - pub fn view_dialog_menu (&self) -> impl Content { - let options = ||["Projects", "Settings", "Help", "Quit"].iter(); - let option = |a,i|Tui::fg(Rgb(255,255,255), format!("{}", a)); - Bsp::s(Tui::bold(true, "tek!"), Bsp::s("", Map::south(1, options, option))) - } - pub fn view_dialog_help <'a> (&'a self, offset: usize) -> impl Content + use<'a> { - Bsp::s(Tui::bold(true, "Help"), "FIXME") - //Bsp::s(Tui::bold(true, "Help"), Bsp::s("", Map::south(1, - //move||self.config.keys.layers.iter() - //.filter_map(|a|(a.0)(self).then_some(a.1)) - //.flat_map(|a|a) - //.filter_map(|x|if let Value::Exp(_, iter)=x.value{ Some(iter) } else { None }) - //.skip(offset) - //.take(20), - //|mut b,i|Fixed::x(60, Align::w(Bsp::e("(", Bsp::e( - //b.next().map(|t|Fixed::x(16, Align::w(Tui::fg(Rgb(64,224,0), format!("{}", t.value))))), - //Bsp::e(" ", Align::w(format!("{}", b.0.0.trim())))))))))) - } - pub fn view_dialog_device (&self, index: usize) -> impl Content + use<'_> { - let choices = ||device_kinds().iter(); - let choice = move|label, i| - Fill::x(Tui::bg(if i == index { Rgb(64,128,32) } else { Rgb(0,0,0) }, - Bsp::e(if i == index { "[ " } else { " " }, - Bsp::w(if i == index { " ]" } else { " " }, - label)))); - Bsp::s(Tui::bold(true, "Add device"), Map::south(1, choices, choice)) - } - pub fn view_dialog_message <'a> (&'a self, message: &'a Message) -> impl Content + use<'a> { - Bsp::s(message, Bsp::s("", "[ OK ]")) - } - pub fn view_dialog_browser <'a> (&'a self, target: &BrowserTarget, browser: &'a Browser) -> impl Content + use<'a> { - Bsp::s( - Padding::xy(3, 1, Fill::x(Align::w(FieldV( - Default::default(), - match target { - BrowserTarget::SaveProject => "Save project:", - BrowserTarget::LoadProject => "Load project:", - BrowserTarget::ImportSample(_) => "Import sample:", - BrowserTarget::ExportSample(_) => "Export sample:", - BrowserTarget::ImportClip(_) => "Import clip:", - BrowserTarget::ExportClip(_) => "Export clip:", - }, - Shrink::x(3, Fixed::y(1, Tui::fg(Tui::g(96), RepeatH("🭻")))))))), - Outer(true, Style::default().fg(Tui::g(96))) - .enclose(Fill::xy(browser))) - } - pub fn view_dialog_load <'a> (&'a self, browser: &'a Browser) -> impl Content + use<'a> { - Bsp::s( - Fill::x(Align::w(Margin::xy(1, 1, Bsp::e( - Tui::bold(true, " Load project: "), - Shrink::x(3, Fixed::y(1, RepeatH("🭻"))))))), - Outer(true, Style::default().fg(Tui::g(96))) - .enclose(Fill::xy(browser))) - } - pub fn view_dialog_export <'a> (&'a self, browser: &'a Browser) -> impl Content + use<'a> { - Bsp::s( - Fill::x(Align::w(Margin::xy(1, 1, Bsp::e( - Tui::bold(true, " Export: "), - Shrink::x(3, Fixed::y(1, RepeatH("🭻"))))))), - Outer(true, Style::default().fg(Tui::g(96))) - .enclose(Fill::xy(browser))) - } - pub fn view_dialog_import <'a> (&'a self, browser: &'a Browser) -> impl Content + use<'a> { - Bsp::s( - Fill::x(Align::w(Margin::xy(1, 1, Bsp::e( - Tui::bold(true, " Import: "), - Shrink::x(3, Fixed::y(1, RepeatH("🭻"))))))), - Outer(true, Style::default().fg(Tui::g(96))) - .enclose(Fill::xy(browser))) - } - pub fn view_dialog_options <'a> (&'a self) -> impl Content + use<'a> { - "TODO" - } -} diff --git a/crates/device/src/dialog/dialog_api.rs b/crates/device/src/dialog/dialog_api.rs new file mode 100644 index 00000000..9c39a26a --- /dev/null +++ b/crates/device/src/dialog/dialog_api.rs @@ -0,0 +1,9 @@ +use crate::*; + +#[tengri_proc::command(Option)] +impl DialogCommand { + fn dismiss (dialog: &mut Option) -> Perhaps { + *dialog = None; + Ok(None) + } +} diff --git a/crates/device/src/dialog/dialog_view.rs b/crates/device/src/dialog/dialog_view.rs new file mode 100644 index 00000000..dadeadbb --- /dev/null +++ b/crates/device/src/dialog/dialog_view.rs @@ -0,0 +1,96 @@ +use crate::*; + +content!(TuiOut: |self: Dialog| match self { + Self::Menu(_) => + self.view_dialog_menu().boxed(), + Self::Help(offset) => + self.view_dialog_help(*offset).boxed(), + Self::Browser(target, browser) => + self.view_dialog_browser(target, browser).boxed(), + Self::Options => + self.view_dialog_options().boxed(), + Self::Device(index) => + self.view_dialog_device(*index).boxed(), + Self::Message(message) => + self.view_dialog_message(message).boxed(), +}); + +content!(TuiOut: |self: Message| match self { + Self::FailedToAddDevice => "Failed to add device." +}); + +impl Dialog { + pub fn view_dialog_menu (&self) -> impl Content { + let options = ||["Projects", "Settings", "Help", "Quit"].iter(); + let option = |a,i|Tui::fg(Rgb(255,255,255), format!("{}", a)); + Bsp::s(Tui::bold(true, "tek!"), Bsp::s("", Map::south(1, options, option))) + } + pub fn view_dialog_help <'a> (&'a self, offset: usize) -> impl Content + use<'a> { + Bsp::s(Tui::bold(true, "Help"), "FIXME") + //Bsp::s(Tui::bold(true, "Help"), Bsp::s("", Map::south(1, + //move||self.config.keys.layers.iter() + //.filter_map(|a|(a.0)(self).then_some(a.1)) + //.flat_map(|a|a) + //.filter_map(|x|if let Value::Exp(_, iter)=x.value{ Some(iter) } else { None }) + //.skip(offset) + //.take(20), + //|mut b,i|Fixed::x(60, Align::w(Bsp::e("(", Bsp::e( + //b.next().map(|t|Fixed::x(16, Align::w(Tui::fg(Rgb(64,224,0), format!("{}", t.value))))), + //Bsp::e(" ", Align::w(format!("{}", b.0.0.trim())))))))))) + } + pub fn view_dialog_device (&self, index: usize) -> impl Content + use<'_> { + let choices = ||device_kinds().iter(); + let choice = move|label, i| + Fill::x(Tui::bg(if i == index { Rgb(64,128,32) } else { Rgb(0,0,0) }, + Bsp::e(if i == index { "[ " } else { " " }, + Bsp::w(if i == index { " ]" } else { " " }, + label)))); + Bsp::s(Tui::bold(true, "Add device"), Map::south(1, choices, choice)) + } + pub fn view_dialog_message <'a> (&'a self, message: &'a Message) -> impl Content + use<'a> { + Bsp::s(message, Bsp::s("", "[ OK ]")) + } + pub fn view_dialog_browser <'a> (&'a self, target: &BrowserTarget, browser: &'a Browser) -> impl Content + use<'a> { + Bsp::s( + Padding::xy(3, 1, Fill::x(Align::w(FieldV( + Default::default(), + match target { + BrowserTarget::SaveProject => "Save project:", + BrowserTarget::LoadProject => "Load project:", + BrowserTarget::ImportSample(_) => "Import sample:", + BrowserTarget::ExportSample(_) => "Export sample:", + BrowserTarget::ImportClip(_) => "Import clip:", + BrowserTarget::ExportClip(_) => "Export clip:", + }, + Shrink::x(3, Fixed::y(1, Tui::fg(Tui::g(96), RepeatH("🭻")))))))), + Outer(true, Style::default().fg(Tui::g(96))) + .enclose(Fill::xy(browser))) + } + pub fn view_dialog_load <'a> (&'a self, browser: &'a Browser) -> impl Content + use<'a> { + Bsp::s( + Fill::x(Align::w(Margin::xy(1, 1, Bsp::e( + Tui::bold(true, " Load project: "), + Shrink::x(3, Fixed::y(1, RepeatH("🭻"))))))), + Outer(true, Style::default().fg(Tui::g(96))) + .enclose(Fill::xy(browser))) + } + pub fn view_dialog_export <'a> (&'a self, browser: &'a Browser) -> impl Content + use<'a> { + Bsp::s( + Fill::x(Align::w(Margin::xy(1, 1, Bsp::e( + Tui::bold(true, " Export: "), + Shrink::x(3, Fixed::y(1, RepeatH("🭻"))))))), + Outer(true, Style::default().fg(Tui::g(96))) + .enclose(Fill::xy(browser))) + } + pub fn view_dialog_import <'a> (&'a self, browser: &'a Browser) -> impl Content + use<'a> { + Bsp::s( + Fill::x(Align::w(Margin::xy(1, 1, Bsp::e( + Tui::bold(true, " Import: "), + Shrink::x(3, Fixed::y(1, RepeatH("🭻"))))))), + Outer(true, Style::default().fg(Tui::g(96))) + .enclose(Fill::xy(browser))) + } + pub fn view_dialog_options <'a> (&'a self) -> impl Content + use<'a> { + "TODO" + } +} diff --git a/deps/tengri b/deps/tengri index 921378b6..3bc73932 160000 --- a/deps/tengri +++ b/deps/tengri @@ -1 +1 @@ -Subproject commit 921378b6dbb38d4f301f688abd1cfef9bdc0f941 +Subproject commit 3bc739328eed0c8fa67b432c7354c7929ddb505f