Initial public khodo snapshot
ci / checks (push) Successful in 38m58s

This commit is contained in:
khodowany
2026-09-04 19:30:11 -05:00
commit 4f9a269f85
99 changed files with 59585 additions and 0 deletions
+495
View File
@@ -0,0 +1,495 @@
#include <vamp_zero/ui.hpp>
#include <algorithm>
#include <cstdint>
#include <expected>
#include <memory>
#include <mutex>
#include <optional>
#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/library.hpp>
#include <vamp_zero/score.hpp>
namespace {
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;
}
try {
return static_cast<std::uint16_t>(std::stoul(bind.substr(colon + 1U)));
} catch (...) {
return 8080;
}
}
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;
}
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.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.practice.exercises.size()) + " EXERCISES", state.selected_main_menu_index == 1U);
}
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.performance.groups.size()) + " SONGS " + std::to_string(state.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.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.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.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 const& part = group->parts[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}, 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.practice.types.size()) + " TYPES " + std::to_string(state.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.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.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.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 const& exercise = lesson->exercises[index - 1U];
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->key_signature.empty() ? std::string{"SOURCE C"} : "SOURCE " + exercise->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) -> void {
auto source_json = std::string{};
auto source_path = std::string{};
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;
}
source_json = part->source_json;
source_path = part->source_path;
} 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;
}
source_json = variant->source_json;
source_path = variant->source_path;
}
auto parsed = vamp_zero::parse_score_document(source_json, source_path);
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);
}
class vamp_root_control final : public luma::control {
public:
explicit vamp_root_control(vamp_zero::ui_state state) : state_{std::move(state)} {}
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 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(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);
} else {
draw_main_menu(context, content, state_.app);
}
}
private:
vamp_zero::ui_state state_{};
};
}
vamp_zero::zero_ui::zero_ui(boost::asio::any_io_executor executor, luma::application_config config) : executor_{std::move(executor)}, application_{std::move(config)} {}
auto vamp_zero::zero_ui::run() -> std::expected<void, modulus::error> {
auto state = snapshot();
auto root = make_root(state);
if (!root) {
return std::unexpected(root.error());
}
if (auto status = application_.set_root(std::move(*root)); !status) {
return status;
}
return application_.run();
}
auto vamp_zero::zero_ui::stop() -> void {
application_.request_stop();
}
auto vamp_zero::zero_ui::publish(vamp_zero::ui_state state) -> std::expected<void, modulus::error> {
{
auto lock = std::scoped_lock{mutex_};
state_ = std::move(state);
}
return application_.dispatch([this] -> std::expected<void, modulus::error> {
auto root = make_root(snapshot());
if (!root) {
return std::unexpected(root.error());
}
return application_.set_root(std::move(*root));
});
}
auto vamp_zero::zero_ui::snapshot() const -> vamp_zero::ui_state {
auto lock = std::scoped_lock{mutex_};
return 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);
}
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),
};
}