+112
-15
@@ -21,6 +21,7 @@
|
||||
|
||||
#include <vamp_zero/errors.hpp>
|
||||
#include <vamp_zero/library.hpp>
|
||||
#include <vamp_zero/score.hpp>
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -108,6 +109,31 @@ auto now_ms() -> std::int64_t {
|
||||
return std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
|
||||
}
|
||||
|
||||
auto execute_delete_stale_catalog_rows(noto::connection& connection, std::string_view table, std::vector<std::string> const& ids) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
|
||||
auto sql = std::string{"DELETE FROM "};
|
||||
sql += table;
|
||||
sql += " WHERE catalog_managed = 1";
|
||||
auto params = std::vector<noto::sqlite_value>{};
|
||||
params.reserve(ids.size());
|
||||
if (!ids.empty()) {
|
||||
sql += " AND id NOT IN (";
|
||||
for (auto index = std::size_t{}; index < ids.size(); ++index) {
|
||||
if (index != 0U) {
|
||||
sql += ", ";
|
||||
}
|
||||
sql += "?";
|
||||
sql += std::to_string(index + 1U);
|
||||
params.emplace_back(ids[index]);
|
||||
}
|
||||
sql += ")";
|
||||
}
|
||||
auto deleted = co_await connection.async_execute(std::move(sql), std::move(params));
|
||||
if (!deleted) {
|
||||
co_return std::unexpected(deleted.error());
|
||||
}
|
||||
co_return std::expected<void, modulus::error>{};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
auto vamp_zero::split_migration_statements(std::string const& input) -> std::expected<std::vector<std::string>, modulus::error> {
|
||||
@@ -203,6 +229,10 @@ auto vamp_zero::async_migrate_database(boost::asio::any_io_executor executor, va
|
||||
if (!practice_hierarchy_statements) {
|
||||
co_return std::unexpected(practice_hierarchy_statements.error());
|
||||
}
|
||||
auto catalog_managed_statements = read_migration(config, "005_catalog_managed_rows.sql");
|
||||
if (!catalog_managed_statements) {
|
||||
co_return std::unexpected(catalog_managed_statements.error());
|
||||
}
|
||||
auto migrator = noto::migrator{connection};
|
||||
auto migrations = std::vector<noto::migration>{};
|
||||
auto foundation = noto::migration{};
|
||||
@@ -237,12 +267,20 @@ auto vamp_zero::async_migrate_database(boost::asio::any_io_executor executor, va
|
||||
co_return co_await execute_statements(connection, statements);
|
||||
};
|
||||
migrations.push_back(std::move(practice_hierarchy));
|
||||
auto catalog_managed = noto::migration{};
|
||||
catalog_managed.id = "005_catalog_managed_rows";
|
||||
catalog_managed.description = "mark rows managed by bundled catalog import";
|
||||
catalog_managed.checksum = "005_catalog_managed_rows_v1";
|
||||
catalog_managed.up = [statements = std::move(*catalog_managed_statements)](noto::connection& connection) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
|
||||
co_return co_await execute_statements(connection, statements);
|
||||
};
|
||||
migrations.push_back(std::move(catalog_managed));
|
||||
|
||||
auto migrated = co_await migrator.async_migrate(std::move(migrations));
|
||||
if (!migrated) {
|
||||
co_return std::unexpected(migrated.error());
|
||||
}
|
||||
auto version = co_await connection.async_set_user_version(4);
|
||||
auto version = co_await connection.async_set_user_version(5);
|
||||
if (!version) {
|
||||
co_return std::unexpected(version.error());
|
||||
}
|
||||
@@ -268,21 +306,14 @@ auto vamp_zero::async_import_catalogs(boost::asio::any_io_executor executor, vam
|
||||
co_return std::unexpected(begin.error());
|
||||
}
|
||||
auto imported = std::expected<void, modulus::error>{};
|
||||
auto cleared = co_await connection.async_execute("DELETE FROM songs");
|
||||
if (!cleared) {
|
||||
imported = std::unexpected(cleared.error());
|
||||
}
|
||||
if (imported) {
|
||||
auto cleared_practice = co_await connection.async_execute("DELETE FROM practice_exercises");
|
||||
if (!cleared_practice) {
|
||||
imported = std::unexpected(cleared_practice.error());
|
||||
}
|
||||
}
|
||||
auto timestamp = now_ms();
|
||||
auto song_ids = std::vector<std::string>{};
|
||||
song_ids.reserve(performance->parts.size());
|
||||
for (auto const& part : performance->parts) {
|
||||
if (!imported) {
|
||||
break;
|
||||
}
|
||||
song_ids.push_back(part.id);
|
||||
auto params = std::vector<noto::sqlite_value>{
|
||||
noto::sqlite_value{part.id},
|
||||
noto::sqlite_value{part.title},
|
||||
@@ -294,16 +325,41 @@ auto vamp_zero::async_import_catalogs(boost::asio::any_io_executor executor, vam
|
||||
noto::sqlite_value{part.source_path},
|
||||
noto::sqlite_value{part.key_signature},
|
||||
};
|
||||
auto inserted = co_await connection.async_execute("INSERT INTO songs(id, title, composer, source_json, created_at_ms, updated_at_ms, part, source_path, key_signature) VALUES(?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)", std::move(params));
|
||||
auto inserted = co_await connection.async_execute(
|
||||
R"sql(
|
||||
INSERT INTO songs(id, title, composer, source_json, created_at_ms, updated_at_ms, part, source_path, key_signature, catalog_managed)
|
||||
VALUES(?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, 1)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
title = excluded.title,
|
||||
composer = excluded.composer,
|
||||
source_json = excluded.source_json,
|
||||
part = excluded.part,
|
||||
source_path = excluded.source_path,
|
||||
key_signature = excluded.key_signature,
|
||||
catalog_managed = 1,
|
||||
updated_at_ms = CASE
|
||||
WHEN songs.title != excluded.title
|
||||
OR songs.composer != excluded.composer
|
||||
OR songs.source_json != excluded.source_json
|
||||
OR songs.part != excluded.part
|
||||
OR songs.source_path != excluded.source_path
|
||||
OR songs.key_signature != excluded.key_signature
|
||||
THEN excluded.updated_at_ms
|
||||
ELSE songs.updated_at_ms
|
||||
END
|
||||
)sql",
|
||||
std::move(params));
|
||||
if (!inserted) {
|
||||
imported = std::unexpected(inserted.error());
|
||||
}
|
||||
}
|
||||
auto practice_ids = std::vector<std::string>{};
|
||||
for (auto const& exercise : practice->exercises) {
|
||||
if (!imported) {
|
||||
break;
|
||||
}
|
||||
for (auto const& variant : exercise.variants) {
|
||||
practice_ids.push_back(variant.id);
|
||||
auto params = std::vector<noto::sqlite_value>{
|
||||
noto::sqlite_value{variant.id},
|
||||
noto::sqlite_value{exercise.practice_type_id},
|
||||
@@ -318,13 +374,54 @@ auto vamp_zero::async_import_catalogs(boost::asio::any_io_executor executor, vam
|
||||
noto::sqlite_value{timestamp},
|
||||
noto::sqlite_value{timestamp},
|
||||
};
|
||||
auto inserted = co_await connection.async_execute("INSERT INTO practice_exercises(id, practice_type_id, practice_type_title, lesson_group_id, lesson_group_title, title, composer, key_signature, source_json, source_path, created_at_ms, updated_at_ms) VALUES(?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)", std::move(params));
|
||||
auto inserted = co_await connection.async_execute(
|
||||
R"sql(
|
||||
INSERT INTO practice_exercises(id, practice_type_id, practice_type_title, lesson_group_id, lesson_group_title, title, composer, key_signature, source_json, source_path, created_at_ms, updated_at_ms, catalog_managed)
|
||||
VALUES(?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, 1)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
practice_type_id = excluded.practice_type_id,
|
||||
practice_type_title = excluded.practice_type_title,
|
||||
lesson_group_id = excluded.lesson_group_id,
|
||||
lesson_group_title = excluded.lesson_group_title,
|
||||
title = excluded.title,
|
||||
composer = excluded.composer,
|
||||
key_signature = excluded.key_signature,
|
||||
source_json = excluded.source_json,
|
||||
source_path = excluded.source_path,
|
||||
catalog_managed = 1,
|
||||
updated_at_ms = CASE
|
||||
WHEN practice_exercises.practice_type_id != excluded.practice_type_id
|
||||
OR practice_exercises.practice_type_title != excluded.practice_type_title
|
||||
OR practice_exercises.lesson_group_id != excluded.lesson_group_id
|
||||
OR practice_exercises.lesson_group_title != excluded.lesson_group_title
|
||||
OR practice_exercises.title != excluded.title
|
||||
OR practice_exercises.composer != excluded.composer
|
||||
OR practice_exercises.key_signature != excluded.key_signature
|
||||
OR practice_exercises.source_json != excluded.source_json
|
||||
OR practice_exercises.source_path != excluded.source_path
|
||||
THEN excluded.updated_at_ms
|
||||
ELSE practice_exercises.updated_at_ms
|
||||
END
|
||||
)sql",
|
||||
std::move(params));
|
||||
if (!inserted) {
|
||||
imported = std::unexpected(inserted.error());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (imported) {
|
||||
auto deleted = co_await execute_delete_stale_catalog_rows(connection, "songs", song_ids);
|
||||
if (!deleted) {
|
||||
imported = std::unexpected(deleted.error());
|
||||
}
|
||||
}
|
||||
if (imported) {
|
||||
auto deleted = co_await execute_delete_stale_catalog_rows(connection, "practice_exercises", practice_ids);
|
||||
if (!deleted) {
|
||||
imported = std::unexpected(deleted.error());
|
||||
}
|
||||
}
|
||||
if (imported) {
|
||||
auto committed = co_await connection.async_execute("COMMIT");
|
||||
if (!committed) {
|
||||
@@ -380,7 +477,7 @@ auto vamp_zero::async_load_performance_library(boost::asio::any_io_executor exec
|
||||
.source_path = text_value(row[4]),
|
||||
.source_json = std::move(source_json),
|
||||
.measure_count = parsed->measures.size(),
|
||||
.total_pages = parsed->measures.empty() ? 1U : (parsed->measures.size() + 11U) / 12U,
|
||||
.total_pages = vamp_zero::score_page_count(*parsed),
|
||||
});
|
||||
}
|
||||
auto closed = co_await connection.async_close();
|
||||
@@ -428,7 +525,7 @@ auto vamp_zero::async_load_practice_library(boost::asio::any_io_executor executo
|
||||
.source_path = source_path,
|
||||
.source_json = source_json,
|
||||
.measure_count = parsed->measures.size(),
|
||||
.total_pages = parsed->measures.empty() ? 1U : (parsed->measures.size() + 11U) / 12U,
|
||||
.total_pages = vamp_zero::score_page_count(*parsed),
|
||||
};
|
||||
exercises.push_back({
|
||||
.id = logical_practice_exercise_id(type_id, lesson_id, source_path, text_value(row[5])),
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#include <boost/system/error_code.hpp>
|
||||
|
||||
#include <vamp_zero/errors.hpp>
|
||||
#include <vamp_zero/score.hpp>
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -273,11 +274,6 @@ auto parse_measure(boost::json::value const& value) -> std::expected<vamp_zero::
|
||||
return measure;
|
||||
}
|
||||
|
||||
auto score_page_count(std::size_t measure_count) -> std::size_t {
|
||||
constexpr auto measures_per_page = std::size_t{12};
|
||||
return measure_count == 0U ? 1U : (measure_count + measures_per_page - 1U) / measures_per_page;
|
||||
}
|
||||
|
||||
auto load_score_documents(std::filesystem::path const& directory, std::bool_t required) -> std::expected<std::vector<vamp_zero::score_document>, modulus::error> {
|
||||
auto error = std::error_code{};
|
||||
if (!std::filesystem::exists(directory, error)) {
|
||||
@@ -559,7 +555,7 @@ auto vamp_zero::load_performance_library(std::string const& assets_path) -> std:
|
||||
.source_path = document.source_path,
|
||||
.source_json = std::move(document.source_json),
|
||||
.measure_count = document.measures.size(),
|
||||
.total_pages = score_page_count(document.measures.size()),
|
||||
.total_pages = vamp_zero::score_page_count(document),
|
||||
});
|
||||
}
|
||||
return vamp_zero::group_library_parts(std::move(parts));
|
||||
@@ -616,7 +612,7 @@ auto vamp_zero::load_practice_library(std::string const& assets_path) -> std::ex
|
||||
.source_path = document->source_path,
|
||||
.source_json = document->source_json,
|
||||
.measure_count = document->measures.size(),
|
||||
.total_pages = score_page_count(document->measures.size()),
|
||||
.total_pages = vamp_zero::score_page_count(*document),
|
||||
};
|
||||
exercises.push_back({
|
||||
.id = std::move(exercise_id),
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
#include <vamp_zero/navigation.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace {
|
||||
|
||||
auto menu_row_count(std::size_t item_count) -> std::size_t {
|
||||
return item_count + 1U;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
auto vamp_zero::selected_group(vamp_zero::app_state const& state) -> vamp_zero::song_group const* {
|
||||
if (state.selected_song_index == 0U || state.performance.groups.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
auto index = state.selected_song_index - 1U;
|
||||
if (index >= state.performance.groups.size()) {
|
||||
return nullptr;
|
||||
}
|
||||
return &state.performance.groups[index];
|
||||
}
|
||||
|
||||
auto vamp_zero::selected_part(vamp_zero::app_state const& state) -> vamp_zero::library_part const* {
|
||||
auto const* group = vamp_zero::selected_group(state);
|
||||
if (state.selected_part_index == 0U || group == nullptr || group->parts.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
auto index = state.selected_part_index - 1U;
|
||||
if (index >= group->parts.size()) {
|
||||
return nullptr;
|
||||
}
|
||||
return &group->parts[index];
|
||||
}
|
||||
|
||||
auto vamp_zero::active_part(vamp_zero::app_state const& state) -> vamp_zero::library_part const* {
|
||||
if (state.active_part_id.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
auto found = std::find_if(state.performance.parts.begin(), state.performance.parts.end(), [&](auto const& part) {
|
||||
return part.id == state.active_part_id;
|
||||
});
|
||||
return found == state.performance.parts.end() ? nullptr : &*found;
|
||||
}
|
||||
|
||||
auto vamp_zero::selected_practice_type(vamp_zero::app_state const& state) -> vamp_zero::practice_type_group const* {
|
||||
if (state.selected_practice_type_index == 0U || state.practice.types.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
auto index = state.selected_practice_type_index - 1U;
|
||||
if (index >= state.practice.types.size()) {
|
||||
return nullptr;
|
||||
}
|
||||
return &state.practice.types[index];
|
||||
}
|
||||
|
||||
auto vamp_zero::selected_practice_lesson(vamp_zero::app_state const& state) -> vamp_zero::practice_lesson_group const* {
|
||||
auto const* type = vamp_zero::selected_practice_type(state);
|
||||
if (state.selected_practice_lesson_index == 0U || type == nullptr || type->lesson_groups.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
auto index = state.selected_practice_lesson_index - 1U;
|
||||
if (index >= type->lesson_groups.size()) {
|
||||
return nullptr;
|
||||
}
|
||||
return &type->lesson_groups[index];
|
||||
}
|
||||
|
||||
auto vamp_zero::selected_practice_exercise(vamp_zero::app_state const& state) -> vamp_zero::practice_exercise const* {
|
||||
auto const* lesson = vamp_zero::selected_practice_lesson(state);
|
||||
if (state.selected_practice_exercise_index == 0U || lesson == nullptr || lesson->exercises.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
auto index = state.selected_practice_exercise_index - 1U;
|
||||
if (index >= lesson->exercises.size()) {
|
||||
return nullptr;
|
||||
}
|
||||
return &lesson->exercises[index];
|
||||
}
|
||||
|
||||
auto vamp_zero::active_practice(vamp_zero::app_state const& state) -> vamp_zero::practice_exercise const* {
|
||||
if (state.active_practice_id.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
auto found = std::find_if(state.practice.exercises.begin(), state.practice.exercises.end(), [&](auto const& exercise) {
|
||||
return exercise.id == state.active_practice_id;
|
||||
});
|
||||
return found == state.practice.exercises.end() ? nullptr : &*found;
|
||||
}
|
||||
|
||||
auto vamp_zero::supports_key_selection(vamp_zero::practice_exercise const& exercise) -> std::bool_t {
|
||||
return vamp_zero::practice_type_supports_key_selection(exercise.practice_type_id);
|
||||
}
|
||||
|
||||
auto vamp_zero::selected_practice_variant(vamp_zero::app_state const& state, vamp_zero::practice_exercise const& exercise) -> vamp_zero::practice_exercise::score_variant const* {
|
||||
if (exercise.variants.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
if (!vamp_zero::supports_key_selection(exercise)) {
|
||||
return &exercise.variants.front();
|
||||
}
|
||||
if (state.selected_practice_key_index == 0U) {
|
||||
return nullptr;
|
||||
}
|
||||
auto index = state.selected_practice_key_index - 1U;
|
||||
if (index >= exercise.variants.size()) {
|
||||
return nullptr;
|
||||
}
|
||||
return &exercise.variants[index];
|
||||
}
|
||||
|
||||
auto vamp_zero::selected_practice_key(vamp_zero::app_state const& state, vamp_zero::practice_exercise const& exercise) -> std::optional<std::string_view> {
|
||||
auto const* variant = vamp_zero::selected_practice_variant(state, exercise);
|
||||
if (variant == nullptr) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return variant->key_signature;
|
||||
}
|
||||
|
||||
auto vamp_zero::practice_key_row_count(vamp_zero::app_state const& state) -> std::size_t {
|
||||
auto const* exercise = vamp_zero::selected_practice_exercise(state);
|
||||
if (exercise == nullptr || !vamp_zero::supports_key_selection(*exercise)) {
|
||||
return 1U;
|
||||
}
|
||||
return menu_row_count(exercise->variants.size());
|
||||
}
|
||||
|
||||
auto vamp_zero::effective_practice_key(vamp_zero::app_state const& state, vamp_zero::practice_exercise const& exercise) -> std::string {
|
||||
if (auto const* variant = vamp_zero::selected_practice_variant(state, exercise); variant != nullptr && !variant->key_signature.empty()) {
|
||||
return variant->key_signature;
|
||||
}
|
||||
return exercise.key_signature.empty() ? std::string{"C"} : exercise.key_signature;
|
||||
}
|
||||
|
||||
auto vamp_zero::active_pages(vamp_zero::app_state const& state) -> std::size_t {
|
||||
if (state.mode == vamp_zero::app_mode::PERFORMANCE_SCORE) {
|
||||
auto const* part = vamp_zero::active_part(state);
|
||||
return part == nullptr ? 0U : part->total_pages;
|
||||
}
|
||||
if (state.mode == vamp_zero::app_mode::PRACTICE_SCORE) {
|
||||
auto const* exercise = vamp_zero::active_practice(state);
|
||||
auto const* variant = exercise == nullptr ? nullptr : vamp_zero::selected_practice_variant(state, *exercise);
|
||||
return variant == nullptr ? 0U : variant->total_pages;
|
||||
}
|
||||
return 0U;
|
||||
}
|
||||
|
||||
auto vamp_zero::practice_fallback_menu(vamp_zero::app_state const& state) -> vamp_zero::app_mode {
|
||||
if (vamp_zero::selected_practice_lesson(state) != nullptr) {
|
||||
return vamp_zero::app_mode::PRACTICE_EXERCISE_MENU;
|
||||
}
|
||||
if (vamp_zero::selected_practice_type(state) != nullptr) {
|
||||
return vamp_zero::app_mode::PRACTICE_LESSON_MENU;
|
||||
}
|
||||
return vamp_zero::app_mode::PRACTICE_TYPE_MENU;
|
||||
}
|
||||
@@ -14,6 +14,7 @@
|
||||
#include <boost/json/value.hpp>
|
||||
|
||||
#include <vamp_zero/errors.hpp>
|
||||
#include <vamp_zero/navigation.hpp>
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -37,151 +38,6 @@ auto menu_row_count(std::size_t item_count) -> std::size_t {
|
||||
return item_count + 1U;
|
||||
}
|
||||
|
||||
auto selected_group(vamp_zero::app_state const& state) -> vamp_zero::song_group const* {
|
||||
if (state.selected_song_index == 0U || state.performance.groups.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
auto index = state.selected_song_index - 1U;
|
||||
if (index >= state.performance.groups.size()) {
|
||||
return nullptr;
|
||||
}
|
||||
return &state.performance.groups[index];
|
||||
}
|
||||
|
||||
auto selected_part(vamp_zero::app_state const& state) -> vamp_zero::library_part const* {
|
||||
auto const* group = selected_group(state);
|
||||
if (state.selected_part_index == 0U || group == nullptr || group->parts.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
auto index = state.selected_part_index - 1U;
|
||||
if (index >= group->parts.size()) {
|
||||
return nullptr;
|
||||
}
|
||||
return &group->parts[index];
|
||||
}
|
||||
|
||||
auto active_part(vamp_zero::app_state const& state) -> vamp_zero::library_part const* {
|
||||
if (state.active_part_id.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
auto found = std::find_if(state.performance.parts.begin(), state.performance.parts.end(), [&](auto const& part) {
|
||||
return part.id == state.active_part_id;
|
||||
});
|
||||
return found == state.performance.parts.end() ? nullptr : &*found;
|
||||
}
|
||||
|
||||
auto selected_practice_type(vamp_zero::app_state const& state) -> vamp_zero::practice_type_group const* {
|
||||
if (state.selected_practice_type_index == 0U || state.practice.types.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
auto index = state.selected_practice_type_index - 1U;
|
||||
if (index >= state.practice.types.size()) {
|
||||
return nullptr;
|
||||
}
|
||||
return &state.practice.types[index];
|
||||
}
|
||||
|
||||
auto selected_practice_lesson(vamp_zero::app_state const& state) -> vamp_zero::practice_lesson_group const* {
|
||||
auto const* type = selected_practice_type(state);
|
||||
if (state.selected_practice_lesson_index == 0U || type == nullptr || type->lesson_groups.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
auto index = state.selected_practice_lesson_index - 1U;
|
||||
if (index >= type->lesson_groups.size()) {
|
||||
return nullptr;
|
||||
}
|
||||
return &type->lesson_groups[index];
|
||||
}
|
||||
|
||||
auto selected_practice_exercise(vamp_zero::app_state const& state) -> vamp_zero::practice_exercise const* {
|
||||
auto const* lesson = selected_practice_lesson(state);
|
||||
if (state.selected_practice_exercise_index == 0U || lesson == nullptr || lesson->exercises.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
auto index = state.selected_practice_exercise_index - 1U;
|
||||
if (index >= lesson->exercises.size()) {
|
||||
return nullptr;
|
||||
}
|
||||
return &lesson->exercises[index];
|
||||
}
|
||||
|
||||
auto active_practice(vamp_zero::app_state const& state) -> vamp_zero::practice_exercise const* {
|
||||
if (state.active_practice_id.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
auto found = std::find_if(state.practice.exercises.begin(), state.practice.exercises.end(), [&](auto const& exercise) {
|
||||
return exercise.id == state.active_practice_id;
|
||||
});
|
||||
return found == state.practice.exercises.end() ? nullptr : &*found;
|
||||
}
|
||||
|
||||
auto supports_key_selection(vamp_zero::practice_exercise const& exercise) -> std::bool_t {
|
||||
return vamp_zero::practice_type_supports_key_selection(exercise.practice_type_id);
|
||||
}
|
||||
|
||||
auto selected_practice_variant(vamp_zero::app_state const& state, vamp_zero::practice_exercise const& exercise) -> vamp_zero::practice_exercise::score_variant const* {
|
||||
if (exercise.variants.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
if (!supports_key_selection(exercise)) {
|
||||
return &exercise.variants.front();
|
||||
}
|
||||
if (state.selected_practice_key_index == 0U) {
|
||||
return nullptr;
|
||||
}
|
||||
auto index = state.selected_practice_key_index - 1U;
|
||||
if (index >= exercise.variants.size()) {
|
||||
return nullptr;
|
||||
}
|
||||
return &exercise.variants[index];
|
||||
}
|
||||
|
||||
auto selected_practice_key(vamp_zero::app_state const& state, vamp_zero::practice_exercise const& exercise) -> std::optional<std::string_view> {
|
||||
auto const* variant = selected_practice_variant(state, exercise);
|
||||
if (variant == nullptr) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return variant->key_signature;
|
||||
}
|
||||
|
||||
auto practice_key_row_count(vamp_zero::app_state const& state) -> std::size_t {
|
||||
auto const* exercise = selected_practice_exercise(state);
|
||||
if (exercise == nullptr || !supports_key_selection(*exercise)) {
|
||||
return 1U;
|
||||
}
|
||||
return menu_row_count(exercise->variants.size());
|
||||
}
|
||||
|
||||
auto effective_practice_key(vamp_zero::app_state const& state, vamp_zero::practice_exercise const& exercise) -> std::string {
|
||||
if (auto const* variant = selected_practice_variant(state, exercise); variant != nullptr && !variant->key_signature.empty()) {
|
||||
return variant->key_signature;
|
||||
}
|
||||
return exercise.key_signature.empty() ? std::string{"C"} : exercise.key_signature;
|
||||
}
|
||||
|
||||
auto active_pages(vamp_zero::app_state const& state) -> std::size_t {
|
||||
if (state.mode == vamp_zero::app_mode::PERFORMANCE_SCORE) {
|
||||
auto const* part = active_part(state);
|
||||
return part == nullptr ? 0U : part->total_pages;
|
||||
}
|
||||
if (state.mode == vamp_zero::app_mode::PRACTICE_SCORE) {
|
||||
auto const* exercise = active_practice(state);
|
||||
auto const* variant = exercise == nullptr ? nullptr : selected_practice_variant(state, *exercise);
|
||||
return variant == nullptr ? 0U : variant->total_pages;
|
||||
}
|
||||
return 0U;
|
||||
}
|
||||
|
||||
auto practice_fallback_menu(vamp_zero::app_state const& state) -> vamp_zero::app_mode {
|
||||
if (selected_practice_lesson(state) != nullptr) {
|
||||
return vamp_zero::app_mode::PRACTICE_EXERCISE_MENU;
|
||||
}
|
||||
if (selected_practice_type(state) != nullptr) {
|
||||
return vamp_zero::app_mode::PRACTICE_LESSON_MENU;
|
||||
}
|
||||
return vamp_zero::app_mode::PRACTICE_TYPE_MENU;
|
||||
}
|
||||
|
||||
auto clamp_state(vamp_zero::app_state state) -> vamp_zero::app_state {
|
||||
state.selected_main_menu_index = std::min(state.selected_main_menu_index, main_menu_count - 1U);
|
||||
state.selected_song_index = std::min(state.selected_song_index, menu_row_count(state.performance.groups.size()) - 1U);
|
||||
|
||||
+64
-8
@@ -777,6 +777,45 @@ auto effective_time_signature(vamp_zero::score_document const& score, vamp_zero:
|
||||
return "4/4";
|
||||
}
|
||||
|
||||
auto notation_state_changes_before(vamp_zero::score_document const& score, std::size_t measure_index) -> std::bool_t {
|
||||
if (measure_index == 0U || measure_index >= score.measures.size()) {
|
||||
return false;
|
||||
}
|
||||
auto const& previous = score.measures[measure_index - 1U];
|
||||
auto const& current = score.measures[measure_index];
|
||||
return effective_key_signature(score, previous) != effective_key_signature(score, current)
|
||||
|| effective_time_signature(score, &previous) != effective_time_signature(score, ¤t);
|
||||
}
|
||||
|
||||
auto make_score_system_ranges(vamp_zero::score_document const& score) -> std::vector<vamp_zero::score_system_range> {
|
||||
auto ranges = std::vector<vamp_zero::score_system_range>{};
|
||||
if (score.measures.empty()) {
|
||||
return ranges;
|
||||
}
|
||||
|
||||
auto current = vamp_zero::score_system_range{.first_measure = 0U, .measure_count = 0U};
|
||||
for (auto measure_index = std::size_t{}; measure_index < score.measures.size(); ++measure_index) {
|
||||
if (current.measure_count != 0U
|
||||
&& (current.measure_count >= vamp_zero::score_measures_per_system || notation_state_changes_before(score, measure_index))) {
|
||||
ranges.push_back(current);
|
||||
current = vamp_zero::score_system_range{.first_measure = measure_index, .measure_count = 0U};
|
||||
}
|
||||
++current.measure_count;
|
||||
}
|
||||
if (current.measure_count != 0U) {
|
||||
ranges.push_back(current);
|
||||
}
|
||||
return ranges;
|
||||
}
|
||||
|
||||
auto show_time_signature_for_system(vamp_zero::score_document const& score, vamp_zero::score_system_range const& range) -> std::bool_t {
|
||||
if (range.first_measure == 0U) {
|
||||
return true;
|
||||
}
|
||||
return notation_state_changes_before(score, range.first_measure)
|
||||
&& effective_time_signature(score, &score.measures[range.first_measure - 1U]) != effective_time_signature(score, &score.measures[range.first_measure]);
|
||||
}
|
||||
|
||||
auto draw_curve(luma::draw_context& context, luma::point start, luma::point control, luma::point end, std::float32_t thickness, luma::color color) -> void {
|
||||
constexpr auto segments = std::int32_t{10};
|
||||
auto previous = start;
|
||||
@@ -2221,6 +2260,18 @@ auto vamp_zero::notated_accidentals_for_measure(vamp_zero::score_measure const&
|
||||
return accidentals;
|
||||
}
|
||||
|
||||
auto vamp_zero::score_system_ranges(vamp_zero::score_document const& score) -> std::vector<vamp_zero::score_system_range> {
|
||||
return make_score_system_ranges(score);
|
||||
}
|
||||
|
||||
auto vamp_zero::score_page_count(vamp_zero::score_document const& score) -> std::size_t {
|
||||
auto ranges = make_score_system_ranges(score);
|
||||
if (ranges.empty()) {
|
||||
return 1U;
|
||||
}
|
||||
return (ranges.size() + vamp_zero::score_systems_per_page - 1U) / vamp_zero::score_systems_per_page;
|
||||
}
|
||||
|
||||
auto vamp_zero::layout_score_page(luma::rect bounds, vamp_zero::score_document const& score, std::size_t page_index) -> vamp_zero::score_page_layout {
|
||||
auto layout = vamp_zero::score_page_layout{
|
||||
.bounds = bounds,
|
||||
@@ -2240,23 +2291,28 @@ auto vamp_zero::layout_score_page(luma::rect bounds, vamp_zero::score_document c
|
||||
auto const content_y = bounds.y + 84.0F;
|
||||
auto const content_width = std::max<std::float32_t>(spacing * 18.0F, bounds.width - margin * 2.0F);
|
||||
auto const system_height = (bounds.height - 108.0F) / static_cast<std::float32_t>(vamp_zero::score_systems_per_page);
|
||||
auto const first_measure = page_index * vamp_zero::score_measures_per_page;
|
||||
auto const system_ranges = make_score_system_ranges(score);
|
||||
auto const first_system = page_index * vamp_zero::score_systems_per_page;
|
||||
auto note_refs = std::vector<laid_out_note_ref>{};
|
||||
auto measure_refs = std::vector<laid_out_measure_ref>{};
|
||||
|
||||
for (auto system = std::size_t{}; system < vamp_zero::score_systems_per_page; ++system) {
|
||||
auto const system_y = content_y + static_cast<std::float32_t>(system) * system_height;
|
||||
auto const staff_top = system_y + spacing * 4.25F;
|
||||
auto const system_first_measure = first_measure + system * vamp_zero::score_measures_per_system;
|
||||
auto const* first_system_measure = system_first_measure < score.measures.size() ? &score.measures[system_first_measure] : nullptr;
|
||||
auto const range_index = first_system + system;
|
||||
auto const* range = range_index < system_ranges.size() ? &system_ranges[range_index] : nullptr;
|
||||
auto const* first_system_measure = range != nullptr && range->first_measure < score.measures.size() ? &score.measures[range->first_measure] : nullptr;
|
||||
layout_staff(layout, content_x, staff_top, content_width, spacing, system);
|
||||
auto const show_time = page_index == 0U && system == 0U;
|
||||
auto const show_time = range == nullptr ? page_index == 0U && system == 0U : show_time_signature_for_system(score, *range);
|
||||
auto const measure_start_x = layout_system_preamble(layout, score, first_system_measure, content_x, staff_top, spacing, show_time, system);
|
||||
auto const measure_area_width = std::max<std::float32_t>(spacing * 12.0F, content_x + content_width - measure_start_x);
|
||||
auto const measure_width = measure_area_width / static_cast<std::float32_t>(vamp_zero::score_measures_per_system);
|
||||
if (range == nullptr || range->measure_count == 0U) {
|
||||
continue;
|
||||
}
|
||||
auto const measure_width = measure_area_width / static_cast<std::float32_t>(range->measure_count);
|
||||
|
||||
for (auto measure = std::size_t{}; measure < vamp_zero::score_measures_per_system; ++measure) {
|
||||
auto const measure_index = system_first_measure + measure;
|
||||
for (auto measure = std::size_t{}; measure < range->measure_count; ++measure) {
|
||||
auto const measure_index = range->first_measure + measure;
|
||||
auto const x = measure_start_x + static_cast<std::float32_t>(measure) * measure_width;
|
||||
if (measure_index < score.measures.size()) {
|
||||
layout_measure(
|
||||
@@ -2274,7 +2330,7 @@ auto vamp_zero::layout_score_page(luma::rect bounds, vamp_zero::score_document c
|
||||
}
|
||||
|
||||
auto const bar_x = x + measure_width;
|
||||
auto const final_bar = measure + 1U == vamp_zero::score_measures_per_system || measure_index + 1U >= score.measures.size();
|
||||
auto const final_bar = measure + 1U == range->measure_count || measure_index + 1U >= score.measures.size();
|
||||
add_stroke_line(
|
||||
layout,
|
||||
final_bar ? vamp_zero::score_layout_item_role::FINAL_BARLINE : vamp_zero::score_layout_item_role::BARLINE,
|
||||
|
||||
+59
-110
@@ -3,11 +3,13 @@
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <expected>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
|
||||
#include <boost/asio/any_io_executor.hpp>
|
||||
@@ -19,8 +21,47 @@
|
||||
#include <vamp_style/font.hpp>
|
||||
#include <vamp_zero/color.hpp>
|
||||
#include <vamp_zero/library.hpp>
|
||||
#include <vamp_zero/navigation.hpp>
|
||||
#include <vamp_zero/score.hpp>
|
||||
|
||||
struct vamp_zero::score_draw_cache {
|
||||
struct entry {
|
||||
std::string source_json{};
|
||||
std::shared_ptr<vamp_zero::score_document const> document{};
|
||||
};
|
||||
|
||||
auto get(std::string_view source_json, std::string source_path, std::string part_override) -> std::expected<std::shared_ptr<vamp_zero::score_document const>, modulus::error> {
|
||||
auto key = source_path;
|
||||
key.push_back('\n');
|
||||
key += part_override;
|
||||
key.push_back('\n');
|
||||
key += std::to_string(source_json.size());
|
||||
key.push_back('\n');
|
||||
key += std::to_string(std::hash<std::string_view>{}(source_json));
|
||||
|
||||
auto found = entries.find(key);
|
||||
if (found != entries.end() && found->second.source_json == source_json && found->second.document) {
|
||||
return found->second.document;
|
||||
}
|
||||
|
||||
auto parsed = vamp_zero::parse_score_document(source_json, source_path);
|
||||
if (!parsed) {
|
||||
return std::unexpected(parsed.error());
|
||||
}
|
||||
if (!part_override.empty()) {
|
||||
parsed->part = std::move(part_override);
|
||||
}
|
||||
auto document = std::make_shared<vamp_zero::score_document const>(std::move(*parsed));
|
||||
entries[key] = {
|
||||
.source_json = std::string{source_json},
|
||||
.document = document,
|
||||
};
|
||||
return document;
|
||||
}
|
||||
|
||||
std::unordered_map<std::string, entry> entries{};
|
||||
};
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr auto touch_hold_duration = std::chrono::milliseconds{700};
|
||||
@@ -41,102 +82,6 @@ auto bind_port(vamp_zero::config const& config) -> std::uint16_t {
|
||||
}
|
||||
}
|
||||
|
||||
auto active_part(vamp_zero::app_state const& state) -> vamp_zero::library_part const* {
|
||||
auto found = std::find_if(state.performance.parts.begin(), state.performance.parts.end(), [&](auto const& part) {
|
||||
return part.id == state.active_part_id;
|
||||
});
|
||||
return found == state.performance.parts.end() ? nullptr : &*found;
|
||||
}
|
||||
|
||||
auto selected_group(vamp_zero::app_state const& state) -> vamp_zero::song_group const* {
|
||||
if (state.selected_song_index == 0U || state.performance.groups.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
auto index = state.selected_song_index - 1U;
|
||||
if (index >= state.performance.groups.size()) {
|
||||
return nullptr;
|
||||
}
|
||||
return &state.performance.groups[index];
|
||||
}
|
||||
|
||||
auto active_practice(vamp_zero::app_state const& state) -> vamp_zero::practice_exercise const* {
|
||||
auto found = std::find_if(state.practice.exercises.begin(), state.practice.exercises.end(), [&](auto const& exercise) {
|
||||
return exercise.id == state.active_practice_id;
|
||||
});
|
||||
return found == state.practice.exercises.end() ? nullptr : &*found;
|
||||
}
|
||||
|
||||
auto selected_practice_type(vamp_zero::app_state const& state) -> vamp_zero::practice_type_group const* {
|
||||
if (state.selected_practice_type_index == 0U || state.practice.types.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
auto index = state.selected_practice_type_index - 1U;
|
||||
if (index >= state.practice.types.size()) {
|
||||
return nullptr;
|
||||
}
|
||||
return &state.practice.types[index];
|
||||
}
|
||||
|
||||
auto selected_practice_lesson(vamp_zero::app_state const& state) -> vamp_zero::practice_lesson_group const* {
|
||||
auto const* type = selected_practice_type(state);
|
||||
if (state.selected_practice_lesson_index == 0U || type == nullptr || type->lesson_groups.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
auto index = state.selected_practice_lesson_index - 1U;
|
||||
if (index >= type->lesson_groups.size()) {
|
||||
return nullptr;
|
||||
}
|
||||
return &type->lesson_groups[index];
|
||||
}
|
||||
|
||||
auto selected_practice_exercise(vamp_zero::app_state const& state) -> vamp_zero::practice_exercise const* {
|
||||
auto const* lesson = selected_practice_lesson(state);
|
||||
if (state.selected_practice_exercise_index == 0U || lesson == nullptr || lesson->exercises.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
auto index = state.selected_practice_exercise_index - 1U;
|
||||
if (index >= lesson->exercises.size()) {
|
||||
return nullptr;
|
||||
}
|
||||
return &lesson->exercises[index];
|
||||
}
|
||||
|
||||
auto supports_key_selection(vamp_zero::practice_exercise const& exercise) -> std::bool_t {
|
||||
return vamp_zero::practice_type_supports_key_selection(exercise.practice_type_id);
|
||||
}
|
||||
|
||||
auto selected_practice_variant(vamp_zero::app_state const& state, vamp_zero::practice_exercise const& exercise) -> vamp_zero::practice_exercise::score_variant const* {
|
||||
if (exercise.variants.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
if (!supports_key_selection(exercise)) {
|
||||
return &exercise.variants.front();
|
||||
}
|
||||
if (state.selected_practice_key_index == 0U) {
|
||||
return nullptr;
|
||||
}
|
||||
auto index = state.selected_practice_key_index - 1U;
|
||||
if (index >= exercise.variants.size()) {
|
||||
return nullptr;
|
||||
}
|
||||
return &exercise.variants[index];
|
||||
}
|
||||
|
||||
auto selected_practice_key(vamp_zero::app_state const& state, vamp_zero::practice_exercise const& exercise) -> std::optional<std::string_view> {
|
||||
auto const* variant = selected_practice_variant(state, exercise);
|
||||
if (variant == nullptr) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return variant->key_signature;
|
||||
}
|
||||
|
||||
auto effective_practice_key(vamp_zero::app_state const& state, vamp_zero::practice_exercise const& exercise) -> std::string {
|
||||
if (auto const* variant = selected_practice_variant(state, exercise); variant != nullptr && !variant->key_signature.empty()) {
|
||||
return variant->key_signature;
|
||||
}
|
||||
return exercise.key_signature.empty() ? std::string{"C"} : exercise.key_signature;
|
||||
}
|
||||
|
||||
auto status_color(vamp_zero::app_state const& state) -> vamp_style::color {
|
||||
return state.pico_connected ? vamp_style::connected : vamp_style::retrying;
|
||||
}
|
||||
@@ -361,9 +306,10 @@ auto draw_practice_key_menu(luma::draw_context& context, luma::rect bounds, vamp
|
||||
}
|
||||
}
|
||||
|
||||
auto draw_score(luma::draw_context& context, luma::rect bounds, vamp_zero::app_state const& state) -> void {
|
||||
auto source_json = std::string{};
|
||||
auto draw_score(luma::draw_context& context, luma::rect bounds, vamp_zero::app_state const& state, vamp_zero::score_draw_cache& score_cache) -> void {
|
||||
auto source_json = std::string_view{};
|
||||
auto source_path = std::string{};
|
||||
auto part_override = std::string{};
|
||||
if (state.mode == vamp_zero::app_mode::PERFORMANCE_SCORE) {
|
||||
auto const* part = active_part(state);
|
||||
if (part == nullptr) {
|
||||
@@ -385,32 +331,28 @@ auto draw_score(luma::draw_context& context, luma::rect bounds, vamp_zero::app_s
|
||||
}
|
||||
source_json = variant->source_json;
|
||||
source_path = variant->source_path;
|
||||
part_override = "Key: " + effective_practice_key(state, *exercise);
|
||||
}
|
||||
|
||||
auto parsed = vamp_zero::parse_score_document(source_json, source_path);
|
||||
auto parsed = score_cache.get(source_json, source_path, std::move(part_override));
|
||||
if (!parsed) {
|
||||
draw_text(context, bounds.x + 36.0F, bounds.y + 38.0F, "SCORE ERROR", 4, vamp_style::danger, 360.0F);
|
||||
draw_text(context, bounds.x + 38.0F, bounds.y + 86.0F, parsed.error().describe(), 1, vamp_style::muted, bounds.width - 72.0F);
|
||||
return;
|
||||
}
|
||||
if (state.mode == vamp_zero::app_mode::PRACTICE_SCORE) {
|
||||
auto const* exercise = active_practice(state);
|
||||
if (exercise != nullptr) {
|
||||
parsed->part = "Key: " + effective_practice_key(state, *exercise);
|
||||
}
|
||||
}
|
||||
auto paper_bounds = luma::rect{
|
||||
.x = bounds.x + 24.0F,
|
||||
.y = bounds.y + 18.0F,
|
||||
.width = bounds.width - 48.0F,
|
||||
.height = bounds.height - 36.0F,
|
||||
};
|
||||
vamp_zero::draw_score_page(context, paper_bounds, *parsed, state.page_index);
|
||||
vamp_zero::draw_score_page(context, paper_bounds, **parsed, state.page_index);
|
||||
}
|
||||
|
||||
class vamp_root_control final : public luma::control {
|
||||
public:
|
||||
explicit vamp_root_control(vamp_zero::ui_state state, touch_handler on_touch) : state_{std::move(state)}, on_touch_{std::move(on_touch)} {}
|
||||
explicit vamp_root_control(vamp_zero::ui_state state, std::shared_ptr<vamp_zero::score_draw_cache> score_cache, touch_handler on_touch)
|
||||
: state_{std::move(state)}, score_cache_{std::move(score_cache)}, on_touch_{std::move(on_touch)} {}
|
||||
auto measure(luma::draw_context&, luma::theme const&, luma::constraints limits) -> luma::size override {
|
||||
return luma::clamp({.width = 1024.0F, .height = 600.0F}, limits);
|
||||
}
|
||||
@@ -439,7 +381,9 @@ public:
|
||||
} else if (state_.app.mode == vamp_zero::app_mode::PRACTICE_KEY_MENU) {
|
||||
draw_practice_key_menu(context, content, state_.app);
|
||||
} else if (state_.app.mode == vamp_zero::app_mode::PERFORMANCE_SCORE || state_.app.mode == vamp_zero::app_mode::PRACTICE_SCORE) {
|
||||
draw_score(context, content, state_.app);
|
||||
if (score_cache_) {
|
||||
draw_score(context, content, state_.app, *score_cache_);
|
||||
}
|
||||
} else {
|
||||
draw_main_menu(context, content, state_.app);
|
||||
}
|
||||
@@ -449,13 +393,18 @@ public:
|
||||
}
|
||||
private:
|
||||
vamp_zero::ui_state state_{};
|
||||
std::shared_ptr<vamp_zero::score_draw_cache> score_cache_{};
|
||||
touch_handler on_touch_{};
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
vamp_zero::zero_ui::zero_ui(boost::asio::any_io_executor executor, luma::application_config config, vamp_zero::zero_ui::command_handler command_handler)
|
||||
: executor_{std::move(executor)}, tap_timer_{executor_}, application_{std::move(config)}, command_handler_{std::move(command_handler)} {}
|
||||
: executor_{std::move(executor)},
|
||||
tap_timer_{executor_},
|
||||
application_{std::move(config)},
|
||||
command_handler_{std::move(command_handler)},
|
||||
score_cache_{std::make_shared<vamp_zero::score_draw_cache>()} {}
|
||||
|
||||
auto vamp_zero::zero_ui::send_touch_command(vamp_protocol::page_command command) -> void {
|
||||
if (!command_handler_) {
|
||||
@@ -593,7 +542,7 @@ auto vamp_zero::zero_ui::snapshot() const -> vamp_zero::ui_state {
|
||||
}
|
||||
|
||||
auto vamp_zero::zero_ui::make_root(vamp_zero::ui_state const& state) -> std::expected<std::unique_ptr<luma::control>, modulus::error> {
|
||||
return std::make_unique<vamp_root_control>(state, [this](luma::touch_event const& event) {
|
||||
return std::make_unique<vamp_root_control>(state, score_cache_, [this](luma::touch_event const& event) {
|
||||
return handle_touch(event);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user