#include #include #include #include #include #include #include #include #include #include #include #include namespace { auto read_bytes(std::filesystem::path const &path) -> std::string { auto input = std::ifstream{path, std::ios::binary}; BOOST_REQUIRE(input.good()); return {std::istreambuf_iterator{input}, std::istreambuf_iterator{}}; } auto color(luma::color value) -> Color { return {value.r, value.g, value.b, value.a}; } auto rectangle(luma::rect value) -> Rectangle { return {value.x, value.y, value.width, value.height}; } // Uses the same raylib glyph rasterizer and metrics as Luma, without a GPU/window. class image_context final : public luma::draw_context { public: image_context() = default; ~image_context() override { UnloadImage(image); for (auto const &[size, font] : fonts_) { UnloadFontData(font.glyphs, font.glyphCount); MemFree(font.recs); } } auto fill_rect(luma::rect bounds, luma::color value) -> void override { ImageDrawRectangleRec(&image, rectangle(bounds), color(value)); } auto stroke_rect(luma::rect bounds, std::float32_t thickness, luma::color value) -> void override { ImageDrawRectangleLines(&image, rectangle(bounds), static_cast(thickness), color(value)); } auto stroke_line(luma::point from, luma::point to, std::float32_t thickness, luma::color value) -> void override { ImageDrawLineEx(&image, {from.x, from.y}, {to.x, to.y}, static_cast(thickness), color(value)); } auto fill_ellipse(luma::rect bounds, luma::color value) -> void override { BOOST_TEST(bounds.width == bounds.height); ImageDrawCircle(&image, static_cast(bounds.x + bounds.width / 2.0F), static_cast(bounds.y + bounds.height / 2.0F), static_cast(bounds.width / 2.0F), color(value)); } auto stroke_ellipse(luma::rect, std::float32_t, luma::color) -> void override { BOOST_FAIL("unexpected ellipse outline"); } auto draw_texture(luma::texture const &, luma::rect, luma::rect, luma::color) -> void override { BOOST_FAIL("menu must not load score textures"); } auto draw_text(std::string_view text, luma::point origin, luma::text_style const &style, luma::color value) -> void override { BOOST_TEST(style.font == vamp_style::ui_font_path); auto measured = measure_text(text, style); BOOST_REQUIRE(clip_.has_value()); BOOST_TEST(measured.width <= clip_->width); BOOST_TEST(origin.y >= clip_->y); BOOST_TEST(origin.y + measured.height <= clip_->y + clip_->height); auto face = font(style.size); auto owned = std::string{text}; auto x = origin.x; for (auto i = std::size_t{}; i < owned.size();) { auto bytes = std::int32_t{}; auto codepoint = GetCodepointNext(owned.c_str() + i, &bytes); auto index = GetGlyphIndex(face, codepoint); auto const &glyph = face.glyphs[index]; if (codepoint != ' ') { ImageDraw(&image, glyph.image, {0.0F, 0.0F, face.recs[index].width, face.recs[index].height}, {x + static_cast(glyph.offsetX), origin.y + static_cast(glyph.offsetY), face.recs[index].width, face.recs[index].height}, color(value)); } x += (glyph.advanceX == 0 ? face.recs[index].width : static_cast(glyph.advanceX)) + 1.0F; i += static_cast(bytes); } } auto push_clip(luma::rect bounds) -> void override { BOOST_TEST(bounds.x >= 0.0F); BOOST_TEST(bounds.y >= 0.0F); BOOST_TEST(bounds.x + bounds.width <= 1024.0F); BOOST_TEST(bounds.y + bounds.height <= 600.0F); clip_ = bounds; } auto pop_clip() -> void override { clip_.reset(); } auto measure_text(std::string_view text, luma::text_style const &style) const -> luma::size override { auto size = MeasureTextEx(font(style.size), std::string{text}.c_str(), style.size, 1.0F); return {size.x, size.y}; } Image image{GenImageColor(1024, 600, {vamp_style::background.r, vamp_style::background.g, vamp_style::background.b, 255})}; private: auto font(std::float32_t size) const -> Font { auto key = static_cast(std::ceil(size)); auto found = fonts_.find(key); if (found != fonts_.end()) { return found->second; } auto bytes = read_bytes(std::filesystem::path{VAMP_TEST_SOURCE_DIR} / "package/assets/fonts/petaluma/PetalumaScript.otf"); auto face = Font{}; face.baseSize = key; face.glyphs = LoadFontData(reinterpret_cast(bytes.data()), static_cast(bytes.size()), key, nullptr, 95, FONT_DEFAULT, &face.glyphCount); BOOST_REQUIRE(face.glyphs != nullptr); auto atlas = GenImageFontAtlas(face.glyphs, &face.recs, face.glyphCount, key, 4, 0); BOOST_REQUIRE(atlas.data != nullptr); for (auto i = std::int32_t{}; i < face.glyphCount; ++i) { UnloadImage(face.glyphs[i].image); face.glyphs[i].image = ImageFromImage(atlas, face.recs[i]); } UnloadImage(atlas); face.texture.id = 1U; fonts_.emplace(key, face); return face; } mutable std::map fonts_{}; std::optional clip_{}; }; auto preview(vamp_zero::app_state const &state, std::string const &name, vamp_pico::link_status link = vamp_pico::link_status::CONNECTED, std::bool_t busy = false) -> void { auto directory = std::filesystem::path{VAMP_TEST_UI_DIR}; std::filesystem::create_directories(directory); auto context = image_context{}; vamp_zero::draw_header(context, {0.0F, 0.0F, 1024.0F, 600.0F}, state); vamp_zero::draw_menu(context, {0.0F, 80.0F, 1024.0F, 520.0F}, state); BOOST_REQUIRE(ExportImage(context.image, (directory / ("zero_" + name + ".png")).c_str())); auto remote = vamp_pico::remote_display_state{}; BOOST_REQUIRE(vamp_pico::parse_remote_display_body(vamp_zero::remote_display_text(state), remote)); auto title = vamp_pico::title_cache{}; if (remote.title_image[0] != 0) { title.select(remote.title_image.data()); BOOST_REQUIRE(title.accept(remote.title_image.data(), read_bytes(std::filesystem::path{VAMP_TEST_ASSETS_DIR} / "pico/titles" / (std::string{remote.title_image.data()} + ".mask")))); } auto atlas = read_bytes(directory / "static.p4"); auto image = GenImageColor(320, 240, WHITE); auto row = std::array{}; auto ink = std::size_t{}; for (auto y = 0U; y < 240U; ++y) { vamp_pico::render_scanline(row, y, remote, link, busy, title, atlas); for (auto x = 0U; x < 320U; ++x) { auto pixel = row[x]; auto value = Color{static_cast((pixel >> 11U) * 255U / 31U), static_cast(((pixel >> 5U) & 63U) * 255U / 63U), static_cast((pixel & 31U) * 255U / 31U), 255}; ImageDrawPixel(&image, x, y, value); ink += value.r > 128U && value.g > 128U && value.b > 128U ? 1U : 0U; } } BOOST_TEST(ink > 200U); BOOST_TEST(ink < 320U * 240U / 2U); BOOST_REQUIRE(ExportImage(image, (directory / ("pico_" + name + ".png")).c_str())); UnloadImage(image); } } BOOST_AUTO_TEST_CASE(device_menu_rendering_uses_petaluma_and_fits_both_displays) { auto state = vamp_zero::app_state{}; preview(state, "main"); preview(state, "offline", vamp_pico::link_status::OFFLINE); preview(state, "busy", vamp_pico::link_status::CONNECTED, true); state.mode = vamp_zero::app_mode::SETTINGS_MENU; state.pico_connected = true; state.audio.playback_enabled = true; state.audio.metronome_enabled = true; preview(state, "settings"); state.selected_settings_index = 3U; state.audio.tempo_editing = true; preview(state, "tempo"); state.audio.tempo_editing = false; state.mode = vamp_zero::app_mode::PERFORMANCE_SONG_MENU; preview(state, "empty"); auto catalog = std::make_shared(); auto score = boost::json::parse(read_bytes(std::filesystem::path{VAMP_TEST_ASSETS_DIR} / "songs/autumn_leaves_electric.json")); auto title = std::string{score.at("presentation").at("title_image").as_string()}; catalog->parts.push_back({.id = "electric", .title = "Autumn Leaves", .composer = "Joseph Kosma", .part = "Electric", .total_pages = 32U, .title_image = title}); catalog->groups.push_back({"Autumn Leaves", "Joseph Kosma", {0U}}); state.catalog = catalog; state.selected_song_index = 1U; preview(state, "songs"); state.mode = vamp_zero::app_mode::PERFORMANCE_PART_MENU; state.selected_part_index = 1U; preview(state, "parts"); catalog->groups[0].title = std::string(200U, 'M'); catalog->groups[0].composer = std::string(200U, 'W'); preview(state, "long_title"); }