Files
product-vamp/src/vamp_zero/ui.cpp
T
khodowany cee9f45b0c
ci / checks (push) Has been cancelled
Fix
2026-09-19 08:17:25 -05:00

616 lines
26 KiB
C++

#include <vamp_zero/ui.hpp>
#include <algorithm>
#include <cstdint>
#include <expected>
#include <functional>
#include <memory>
#include <mutex>
#include <string>
#include <string_view>
#include <utility>
#include <boost/asio/any_io_executor.hpp>
#include <luma.hpp>
#include <vamp_style.hpp>
#include <vamp_style/font.hpp>
#include <vamp_zero/color.hpp>
#include <vamp_zero/database.hpp>
#include <vamp_zero/errors.hpp>
#include <vamp_zero/navigation.hpp>
#include <vamp_zero/parse.hpp>
#include <vamp_zero/score.hpp>
#include <vamp_zero/score_loader.hpp>
#include <vamp_zero/touch_input.hpp>
struct vamp_zero::ui_runtime {
ui_runtime(boost::asio::any_io_executor executor, std::function<void(vamp_protocol::page_command)> handler)
: touch_{std::move(executor), std::move(handler)} {}
vamp_zero::touch_input touch_;
std::mutex mutex_{};
vamp_zero::ui_state state_{};
};
namespace {
struct score_draw_cache {
explicit score_draw_cache(luma::application &application)
: textures{[this, &application](std::string const &name) -> std::expected<std::shared_ptr<luma::texture>, modulus::error> {
auto score = document.lock();
if (!score) {
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::UNAVAILABLE, "score no longer selected"));
}
auto found = std::find_if(score->pages.begin(), score->pages.end(), [&](auto const &page) { return page.path == name; });
auto path = vamp_zero::validated_page_path(*score, static_cast<std::size_t>(std::distance(score->pages.begin(), found)));
if (!path) {
return std::unexpected(path.error());
}
return application.load_texture_uncached(*path);
}} {}
auto select(std::shared_ptr<vamp_zero::score_document const> const &score) -> void {
if (score && document.lock() == score) {
return;
}
textures.clear();
document = score;
}
auto draw(luma::draw_context &context, luma::rect bounds, std::size_t index) -> std::expected<void, modulus::error> {
auto score = document.lock();
if (!score || index >= score->pages.size()) {
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::INVALID_ARGUMENT, "page index outside score"));
}
auto texture = textures.get(score->pages[index].path);
if (!texture) {
return std::unexpected(texture.error());
}
auto scale = std::min(bounds.width / 976.0F, bounds.height / 484.0F);
auto page = luma::rect{.x = bounds.x + (bounds.width - 976.0F * scale) / 2.0F,
.y = bounds.y + (bounds.height - 484.0F * scale) / 2.0F,
.width = 976.0F * scale,
.height = 484.0F * scale};
context.fill_rect(page, {.r = 255, .g = 255, .b = 255, .a = 255});
context.draw_texture(**texture, {.x = 0, .y = 0, .width = 976, .height = 484}, page, {.r = 255, .g = 255, .b = 255, .a = 255});
return {};
}
vamp_zero::score_page_cache textures;
std::weak_ptr<vamp_zero::score_document const> document{};
};
auto bind_port(vamp_zero::config const &config) -> std::uint16_t {
auto const &bind = config.control.endpoint.bind;
auto colon = bind.rfind(':');
if (colon == std::string::npos) {
return 8080;
}
return vamp_zero::parse_integer<std::uint16_t>(std::string_view{bind}.substr(colon + 1U), "port").value_or(8080);
}
auto status_color(vamp_zero::app_state const &state) -> vamp_style::color {
return state.pico_connected ? vamp_style::connected : vamp_style::retrying;
}
auto text_style(std::int32_t scale) -> luma::text_style {
auto size = 13.0F;
if (scale == 2) {
size = 20.0F;
} else if (scale == 3) {
size = 26.0F;
} else if (scale >= 4) {
size = 34.0F;
}
return {
.font = std::string{vamp_style::ui_font_path},
.size = size,
.line_height = size + 5.0F,
};
}
auto draw_text(luma::draw_context &context, std::float32_t x, std::float32_t y, std::string const &text, std::int32_t scale, vamp_style::color color, std::float32_t max_width) -> void {
if (text.empty() || max_width <= 0.0F) {
return;
}
auto style = text_style(scale);
context.push_clip({.x = x, .y = y, .width = max_width, .height = style.line_height});
context.draw_text(text, {.x = x, .y = y}, style, vamp_zero::to_luma(color));
context.pop_clip();
}
auto draw_header(luma::draw_context &context, luma::rect bounds, vamp_zero::ui_state const &state) -> void {
context.fill_rect(
{.x = bounds.x, .y = bounds.y, .width = bounds.width, .height = 80.0F}, vamp_zero::to_luma(vamp_style::panel));
context.fill_rect({.x = bounds.x, .y = bounds.y + 78.0F, .width = bounds.width, .height = 2.0F},
vamp_zero::to_luma(vamp_style::accent));
draw_text(context, bounds.x + 28.0F, bounds.y + 18.0F, "PRODUCT VAMP", 4, vamp_style::ink, 360.0F);
draw_text(context, bounds.x + 30.0F, bounds.y + 55.0F, "AP " + state.ap_ssid + " " + state.zero_ip + ":" + std::to_string(state.port), 1, vamp_style::muted, 520.0F);
auto indicator = status_color(state.app);
auto x = bounds.x + bounds.width - 148.0F;
context.fill_rect({.x = x, .y = bounds.y + 22.0F, .width = 24.0F, .height = 24.0F}, vamp_zero::to_luma(indicator));
draw_text(context, x + 36.0F, bounds.y + 26.0F, "PICO", 2, vamp_style::ink, 88.0F);
}
auto draw_row(luma::draw_context &context, luma::rect row, std::string const &primary, std::string const &secondary, std::bool_t selected) -> void {
if (selected) {
context.fill_rect(row, vamp_zero::to_luma(vamp_style::selection));
}
context.stroke_line({.x = row.x, .y = row.y + row.height},
{.x = row.x + row.width, .y = row.y + row.height},
1.0F,
vamp_zero::to_luma(vamp_style::rule));
draw_text(context, row.x + 16.0F, row.y + 10.0F, primary, 2, vamp_style::ink, row.width * 0.62F);
if (!secondary.empty()) {
draw_text(context, row.x + row.width - 250.0F, row.y + 17.0F, secondary, 1, selected ? vamp_style::ink : vamp_style::muted, 230.0F);
}
}
auto first_visible_row(std::size_t selected, std::size_t total, std::size_t max_rows) -> std::size_t {
if (total <= max_rows || selected < max_rows) {
return 0U;
}
return selected - max_rows + 1U;
}
auto draw_main_menu(luma::draw_context &context, luma::rect bounds, vamp_zero::app_state const &state) -> void {
draw_text(context, bounds.x + 36.0F, bounds.y + 28.0F, "MODE", 4, vamp_style::ink, 260.0F);
draw_text(context, bounds.x + 38.0F, bounds.y + 66.0F, "SELECT WORKFLOW", 1, vamp_style::muted, 320.0F);
auto list_y = bounds.y + 112.0F;
auto row_height = std::float32_t{54.0F};
auto row_width = bounds.width - 72.0F;
draw_row(context,
{.x = bounds.x + 36.0F, .y = list_y, .width = row_width, .height = row_height},
"Performance Mode",
std::to_string(state.catalog->performance.groups.size()) + " SONGS",
state.selected_main_menu_index == 0U);
draw_row(context,
{.x = bounds.x + 36.0F, .y = list_y + row_height, .width = row_width, .height = row_height},
"Practice Mode",
std::to_string(state.catalog->practice.exercises.size()) + " EXERCISES",
state.selected_main_menu_index == 1U);
draw_row(context,
{.x = bounds.x + 36.0F, .y = list_y + row_height * 2.0F, .width = row_width, .height = row_height},
"Playback",
state.audio.playback_enabled ? "ON" : "OFF",
state.selected_main_menu_index == 2U);
draw_row(context,
{.x = bounds.x + 36.0F, .y = list_y + row_height * 3.0F, .width = row_width, .height = row_height},
"Metronome",
state.audio.metronome_enabled ? "ON" : "OFF",
state.selected_main_menu_index == 3U);
auto tempo = std::to_string(state.audio.metronome_bpm) + " BPM";
if (state.audio.tempo_editing) {
tempo = "EDIT " + tempo;
}
draw_row(context,
{.x = bounds.x + 36.0F, .y = list_y + row_height * 4.0F, .width = row_width, .height = row_height},
"Tempo",
tempo,
state.selected_main_menu_index == 4U);
}
auto draw_performance_song_menu(luma::draw_context &context, luma::rect bounds, vamp_zero::app_state const &state) -> void {
draw_text(context, bounds.x + 36.0F, bounds.y + 28.0F, "PERFORMANCE", 4, vamp_style::ink, 360.0F);
draw_text(context, bounds.x + 38.0F, bounds.y + 66.0F, std::to_string(state.catalog->performance.groups.size()) + " SONGS " + std::to_string(state.catalog->performance.parts.size()) + " PARTS", 1, vamp_style::muted, 320.0F);
auto list_y = bounds.y + 112.0F;
auto row_height = std::float32_t{54.0F};
auto row_width = bounds.width - 72.0F;
auto total_rows = state.catalog->performance.groups.size() + 1U;
auto max_rows = std::min<std::size_t>(total_rows, 7U);
auto first = first_visible_row(state.selected_song_index, total_rows, max_rows);
for (auto row = std::size_t{}; row < max_rows; ++row) {
auto index = first + row;
if (index == 0U) {
draw_row(context,
{.x = bounds.x + 36.0F,
.y = list_y + static_cast<std::float32_t>(row) * row_height,
.width = row_width,
.height = row_height},
"BACK",
"",
state.selected_song_index == 0U);
continue;
}
auto const &group = state.catalog->performance.groups[index - 1U];
draw_row(context,
{.x = bounds.x + 36.0F,
.y = list_y + static_cast<std::float32_t>(row) * row_height,
.width = row_width,
.height = row_height},
group.title,
std::to_string(group.parts.size()) + " PARTS",
index == state.selected_song_index);
}
if (state.catalog->performance.groups.empty()) {
draw_text(context, bounds.x + 38.0F, list_y + row_height + 28.0F, "NO SONGS FOUND", 3, vamp_style::retrying, 420.0F);
}
}
auto draw_performance_part_menu(luma::draw_context &context, luma::rect bounds, vamp_zero::app_state const &state) -> void {
auto const *group = selected_group(state);
draw_text(context,
bounds.x + 36.0F,
bounds.y + 28.0F,
group == nullptr ? std::string{"PARTS"} : group->title,
4,
vamp_style::ink,
bounds.width - 72.0F);
if (group == nullptr) {
draw_text(context, bounds.x + 38.0F, bounds.y + 118.0F, "NO PARTS FOUND", 3, vamp_style::retrying, 420.0F);
return;
}
draw_text(context, bounds.x + 38.0F, bounds.y + 66.0F, group->composer, 1, vamp_style::muted, bounds.width - 72.0F);
auto list_y = bounds.y + 112.0F;
auto row_height = std::float32_t{54.0F};
auto row_width = bounds.width - 72.0F;
auto total_rows = group->parts.size() + 1U;
auto max_rows = std::min<std::size_t>(total_rows, 7U);
auto first = first_visible_row(state.selected_part_index, total_rows, max_rows);
for (auto row = std::size_t{}; row < max_rows; ++row) {
auto index = first + row;
if (index == 0U) {
draw_row(context,
{.x = bounds.x + 36.0F,
.y = list_y + static_cast<std::float32_t>(row) * row_height,
.width = row_width,
.height = row_height},
"BACK",
"",
state.selected_part_index == 0U);
continue;
}
auto part_index = group->parts[index - 1U];
if (part_index >= state.catalog->performance.parts.size()) {
continue;
}
auto const &part = state.catalog->performance.parts[part_index];
draw_row(context,
{.x = bounds.x + 36.0F,
.y = list_y + static_cast<std::float32_t>(row) * row_height,
.width = row_width,
.height = row_height},
part.part,
std::to_string(part.total_pages) + " PAGES",
index == state.selected_part_index);
}
}
auto draw_practice_type_menu(luma::draw_context &context, luma::rect bounds, vamp_zero::app_state const &state) -> void {
draw_text(context, bounds.x + 36.0F, bounds.y + 28.0F, "PRACTICE", 4, vamp_style::ink, 300.0F);
draw_text(context, bounds.x + 38.0F, bounds.y + 66.0F, std::to_string(state.catalog->practice.types.size()) + " TYPES " + std::to_string(state.catalog->practice.exercises.size()) + " EXERCISES", 1, vamp_style::muted, 420.0F);
auto list_y = bounds.y + 112.0F;
auto row_height = std::float32_t{54.0F};
auto row_width = bounds.width - 72.0F;
auto total_rows = state.catalog->practice.types.size() + 1U;
auto max_rows = std::min<std::size_t>(total_rows, 7U);
auto first = first_visible_row(state.selected_practice_type_index, total_rows, max_rows);
for (auto row = std::size_t{}; row < max_rows; ++row) {
auto index = first + row;
if (index == 0U) {
draw_row(context,
{.x = bounds.x + 36.0F,
.y = list_y + static_cast<std::float32_t>(row) * row_height,
.width = row_width,
.height = row_height},
"BACK",
"",
state.selected_practice_type_index == 0U);
continue;
}
auto const &type = state.catalog->practice.types[index - 1U];
auto exercise_count = std::size_t{};
for (auto const &lesson : type.lesson_groups) {
exercise_count += lesson.exercises.size();
}
draw_row(context,
{.x = bounds.x + 36.0F,
.y = list_y + static_cast<std::float32_t>(row) * row_height,
.width = row_width,
.height = row_height},
type.title,
std::to_string(exercise_count) + " EXERCISES",
index == state.selected_practice_type_index);
}
if (state.catalog->practice.types.empty()) {
draw_text(context, bounds.x + 38.0F, list_y + row_height + 28.0F, "NO PRACTICE EXERCISES FOUND", 3, vamp_style::retrying, 560.0F);
}
}
auto draw_practice_lesson_menu(luma::draw_context &context, luma::rect bounds, vamp_zero::app_state const &state) -> void {
auto const *type = selected_practice_type(state);
draw_text(context,
bounds.x + 36.0F,
bounds.y + 28.0F,
type == nullptr ? std::string{"PRACTICE"} : type->title,
4,
vamp_style::ink,
bounds.width - 72.0F);
draw_text(context, bounds.x + 38.0F, bounds.y + 66.0F, "SELECT LESSON GROUP", 1, vamp_style::muted, 360.0F);
if (type == nullptr) {
draw_text(context, bounds.x + 38.0F, bounds.y + 118.0F, "NO LESSON GROUPS FOUND", 3, vamp_style::retrying, 520.0F);
return;
}
auto list_y = bounds.y + 112.0F;
auto row_height = std::float32_t{54.0F};
auto row_width = bounds.width - 72.0F;
auto total_rows = type->lesson_groups.size() + 1U;
auto max_rows = std::min<std::size_t>(total_rows, 7U);
auto first = first_visible_row(state.selected_practice_lesson_index, total_rows, max_rows);
for (auto row = std::size_t{}; row < max_rows; ++row) {
auto index = first + row;
if (index == 0U) {
draw_row(context,
{.x = bounds.x + 36.0F,
.y = list_y + static_cast<std::float32_t>(row) * row_height,
.width = row_width,
.height = row_height},
"BACK",
"",
state.selected_practice_lesson_index == 0U);
continue;
}
auto const &lesson = type->lesson_groups[index - 1U];
draw_row(context,
{.x = bounds.x + 36.0F,
.y = list_y + static_cast<std::float32_t>(row) * row_height,
.width = row_width,
.height = row_height},
lesson.title,
std::to_string(lesson.exercises.size()) + " EXERCISES",
index == state.selected_practice_lesson_index);
}
}
auto draw_practice_exercise_menu(luma::draw_context &context, luma::rect bounds, vamp_zero::app_state const &state) -> void {
auto const *lesson = selected_practice_lesson(state);
draw_text(context,
bounds.x + 36.0F,
bounds.y + 28.0F,
lesson == nullptr ? std::string{"EXERCISES"} : lesson->title,
4,
vamp_style::ink,
bounds.width - 72.0F);
draw_text(context, bounds.x + 38.0F, bounds.y + 66.0F, "SELECT EXERCISE", 1, vamp_style::muted, 320.0F);
if (lesson == nullptr) {
draw_text(context, bounds.x + 38.0F, bounds.y + 118.0F, "NO EXERCISES FOUND", 3, vamp_style::retrying, 440.0F);
return;
}
auto list_y = bounds.y + 112.0F;
auto row_height = std::float32_t{54.0F};
auto row_width = bounds.width - 72.0F;
auto total_rows = lesson->exercises.size() + 1U;
auto max_rows = std::min<std::size_t>(total_rows, 7U);
auto first = first_visible_row(state.selected_practice_exercise_index, total_rows, max_rows);
for (auto row = std::size_t{}; row < max_rows; ++row) {
auto index = first + row;
if (index == 0U) {
draw_row(context,
{.x = bounds.x + 36.0F,
.y = list_y + static_cast<std::float32_t>(row) * row_height,
.width = row_width,
.height = row_height},
"BACK",
"",
state.selected_practice_exercise_index == 0U);
continue;
}
auto exercise_index = lesson->exercises[index - 1U];
if (exercise_index >= state.catalog->practice.exercises.size()) {
continue;
}
auto const &exercise = state.catalog->practice.exercises[exercise_index];
auto secondary =
supports_key_selection(exercise) ? std::string{"KEY SELECT"} : "KEY " + effective_practice_key(state, exercise);
draw_row(context,
{.x = bounds.x + 36.0F,
.y = list_y + static_cast<std::float32_t>(row) * row_height,
.width = row_width,
.height = row_height},
exercise.title,
secondary,
index == state.selected_practice_exercise_index);
}
}
auto draw_practice_key_menu(luma::draw_context &context, luma::rect bounds, vamp_zero::app_state const &state) -> void {
auto const *exercise = selected_practice_exercise(state);
draw_text(context,
bounds.x + 36.0F,
bounds.y + 28.0F,
exercise == nullptr ? std::string{"KEY"} : exercise->title,
4,
vamp_style::ink,
bounds.width - 72.0F);
draw_text(context, bounds.x + 38.0F, bounds.y + 66.0F, "SELECT PRACTICE KEY", 1, vamp_style::muted, 360.0F);
auto list_y = bounds.y + 112.0F;
auto row_height = std::float32_t{54.0F};
auto row_width = bounds.width - 72.0F;
auto total_rows = (exercise == nullptr ? 0U : exercise->variants.size()) + 1U;
auto max_rows = std::min<std::size_t>(total_rows, 7U);
auto first = first_visible_row(state.selected_practice_key_index, total_rows, max_rows);
for (auto row = std::size_t{}; row < max_rows; ++row) {
auto index = first + row;
if (index == 0U) {
draw_row(context,
{.x = bounds.x + 36.0F,
.y = list_y + static_cast<std::float32_t>(row) * row_height,
.width = row_width,
.height = row_height},
"BACK",
"",
state.selected_practice_key_index == 0U);
continue;
}
auto key = exercise == nullptr ? std::string{"C"} : exercise->variants[index - 1U].key_signature;
auto source = exercise == nullptr || exercise->authored_key_signature.empty() ? std::string{}
: "SOURCE " + exercise->authored_key_signature;
draw_row(context,
{.x = bounds.x + 36.0F,
.y = list_y + static_cast<std::float32_t>(row) * row_height,
.width = row_width,
.height = row_height},
key,
source,
index == state.selected_practice_key_index);
}
}
auto draw_score(luma::draw_context &context, luma::rect bounds, vamp_zero::app_state const &state, vamp_zero::score_load_state const &loaded, score_draw_cache &score_cache) -> void {
if (state.mode == vamp_zero::app_mode::PERFORMANCE_SCORE) {
auto const *part = active_part(state);
if (part == nullptr) {
draw_performance_song_menu(context, bounds, state);
return;
}
} else {
auto const *exercise = active_practice(state);
if (exercise == nullptr) {
draw_practice_type_menu(context, bounds, state);
return;
}
auto const *variant = selected_practice_variant(state, *exercise);
if (variant == nullptr) {
draw_practice_key_menu(context, bounds, state);
return;
}
}
if (loaded.loading) {
draw_text(context, bounds.x + 36.0F, bounds.y + 38.0F, "LOADING SCORE", 4, vamp_style::ink, bounds.width - 72.0F);
return;
}
if (loaded.error || !loaded.document) {
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, loaded.error ? loaded.error->describe() : "Score unavailable", 1, vamp_style::muted, bounds.width - 72.0F);
return;
}
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,
};
if (!state.audio.playback_error.empty()) {
draw_text(context, bounds.x + 24.0F, bounds.y + 12.0F, state.audio.playback_error, 1, vamp_style::danger, bounds.width - 48.0F);
paper_bounds.y += 48.0F;
paper_bounds.height -= 48.0F;
}
if (auto status = score_cache.draw(context, paper_bounds, state.page_index); !status) {
draw_text(context, bounds.x + 36.0F, bounds.y + 38.0F, "SCORE PAGE ERROR", 3, vamp_style::danger, bounds.width - 72.0F);
draw_text(context, bounds.x + 38.0F, bounds.y + 86.0F, status.error().describe(), 1, vamp_style::muted, bounds.width - 72.0F);
}
}
class vamp_root_control final : public luma::control {
public:
vamp_root_control(std::shared_ptr<vamp_zero::ui_runtime> runtime, std::shared_ptr<vamp_zero::score_loader> loader, luma::application &application)
: runtime_{std::move(runtime)}, loader_{std::move(loader)}, score_cache_{application} {}
auto measure(luma::draw_context &, luma::theme const &, luma::constraints limits) -> luma::size override {
return luma::clamp({.width = 1024.0F, .height = 600.0F}, limits);
}
auto draw(luma::draw_context &context, luma::theme const &) const -> void override {
auto state = vamp_zero::ui_state{};
auto loaded = vamp_zero::score_load_state{};
{
auto lock = std::scoped_lock{runtime_->mutex_};
state = runtime_->state_;
loaded = loader_->snapshot();
}
score_cache_.select(loaded.document);
auto area = bounds();
context.fill_rect(area, vamp_zero::to_luma(vamp_style::background));
draw_header(context, area, state);
auto content = luma::rect{
.x = area.x,
.y = area.y + 80.0F,
.width = area.width,
.height = std::max<std::float32_t>(0.0F, area.height - 80.0F),
};
if (state.app.mode == vamp_zero::app_mode::MAIN_MENU) {
draw_main_menu(context, content, state.app);
} else if (state.app.mode == vamp_zero::app_mode::PERFORMANCE_SONG_MENU) {
draw_performance_song_menu(context, content, state.app);
} else if (state.app.mode == vamp_zero::app_mode::PERFORMANCE_PART_MENU) {
draw_performance_part_menu(context, content, state.app);
} else if (state.app.mode == vamp_zero::app_mode::PRACTICE_TYPE_MENU) {
draw_practice_type_menu(context, content, state.app);
} else if (state.app.mode == vamp_zero::app_mode::PRACTICE_LESSON_MENU) {
draw_practice_lesson_menu(context, content, state.app);
} else if (state.app.mode == vamp_zero::app_mode::PRACTICE_EXERCISE_MENU) {
draw_practice_exercise_menu(context, content, state.app);
} 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, loaded, score_cache_);
} else {
draw_main_menu(context, content, state.app);
}
}
auto handle_touch(luma::touch_event const &event) -> std::bool_t override {
return runtime_->touch_.handle(event);
}
private:
std::shared_ptr<vamp_zero::ui_runtime> runtime_;
std::shared_ptr<vamp_zero::score_loader> loader_;
mutable score_draw_cache score_cache_;
};
}
vamp_zero::zero_ui::zero_ui(boost::asio::any_io_executor executor, luma::application_config config, vamp_zero::database_config database, std::string assets_path, std::function<void(vamp_protocol::page_command)> command_handler)
: application_{std::make_shared<luma::application>(std::move(config))},
runtime_{std::make_shared<vamp_zero::ui_runtime>(executor, std::move(command_handler))},
score_loader_{std::make_shared<vamp_zero::score_loader>(executor, [executor, database = std::move(database), assets = std::move(assets_path)](vamp_zero::score_reference reference) { return vamp_zero::async_load_score(executor, database, std::move(reference), assets); }, [weak = std::weak_ptr{application_}] {
if (auto application = weak.lock()) {
static_cast<void>(application->request_layout());
} })} {}
vamp_zero::zero_ui::~zero_ui() {
stop();
}
auto vamp_zero::zero_ui::run() -> std::expected<void, modulus::error> {
if (auto status = application_->set_root(std::make_unique<vamp_root_control>(runtime_, score_loader_, *application_)); !status) {
return status;
}
auto status = application_->run();
stop();
return status;
}
auto vamp_zero::zero_ui::stop() -> void {
runtime_->touch_.stop();
score_loader_->stop();
application_->request_stop();
}
auto vamp_zero::zero_ui::publish(vamp_zero::ui_state state) -> std::expected<void, modulus::error> {
{
auto lock = std::scoped_lock{runtime_->mutex_};
if (!state.app.catalog) {
state.app.catalog = vamp_zero::empty_catalogue();
}
score_loader_->select(state.app.catalog, vamp_zero::selected_score_reference(state.app));
runtime_->state_ = std::move(state);
}
return application_->request_layout();
}
auto vamp_zero::zero_ui::snapshot() const -> vamp_zero::ui_state {
auto lock = std::scoped_lock{runtime_->mutex_};
return runtime_->state_;
}
auto vamp_zero::zero_ui::selected_score_loader() const -> std::shared_ptr<vamp_zero::score_loader> {
return score_loader_;
}
auto vamp_zero::make_ui_state(vamp_zero::config const &config, vamp_zero::app_state app) -> vamp_zero::ui_state {
return vamp_zero::ui_state{
.app = std::move(app),
.ap_ssid = config.control.ap_ssid,
.zero_ip = config.control.zero_ip,
.port = bind_port(config),
};
}