mirror of
https://github.com/usatiuk/cardboy.git
synced 2025-10-28 23:27:49 +01:00
178 lines
5.9 KiB
C++
178 lines
5.9 KiB
C++
#include "cardboy/apps/menu_app.hpp"
|
|
|
|
#include "cardboy/sdk/app_framework.hpp"
|
|
#include "cardboy/sdk/app_system.hpp"
|
|
|
|
#include "cardboy/gfx/font16x8.hpp"
|
|
|
|
#include <algorithm>
|
|
#include <cstdlib>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <vector>
|
|
|
|
namespace apps {
|
|
|
|
namespace {
|
|
|
|
using cardboy::sdk::AppContext;
|
|
|
|
using Framebuffer = typename AppContext::Framebuffer;
|
|
|
|
struct MenuEntry {
|
|
std::string name;
|
|
std::size_t index = 0;
|
|
};
|
|
|
|
class MenuApp final : public cardboy::sdk::IApp {
|
|
public:
|
|
explicit MenuApp(AppContext& ctx) : context(ctx), framebuffer(ctx.framebuffer) { refreshEntries(); }
|
|
|
|
void onStart() override {
|
|
refreshEntries();
|
|
dirty = true;
|
|
renderIfNeeded();
|
|
}
|
|
|
|
void handleEvent(const cardboy::sdk::AppEvent& event) override {
|
|
if (event.type != cardboy::sdk::AppEventType::Button)
|
|
return;
|
|
|
|
const auto& current = event.button.current;
|
|
const auto& previous = event.button.previous;
|
|
|
|
if (current.left && !previous.left) {
|
|
moveSelection(-1);
|
|
} else if (current.right && !previous.right) {
|
|
moveSelection(+1);
|
|
}
|
|
|
|
const bool launch = (current.a && !previous.a) || (current.select && !previous.select);
|
|
if (launch)
|
|
launchSelected();
|
|
|
|
renderIfNeeded();
|
|
}
|
|
|
|
private:
|
|
AppContext& context;
|
|
Framebuffer& framebuffer;
|
|
std::vector<MenuEntry> entries;
|
|
std::size_t selected = 0;
|
|
|
|
bool dirty = false;
|
|
|
|
void moveSelection(int step) {
|
|
if (entries.empty())
|
|
return;
|
|
const int count = static_cast<int>(entries.size());
|
|
int next = static_cast<int>(selected) + step;
|
|
next = (next % count + count) % count;
|
|
selected = static_cast<std::size_t>(next);
|
|
dirty = true;
|
|
}
|
|
|
|
void launchSelected() {
|
|
if (entries.empty())
|
|
return;
|
|
const auto target = entries[selected].index;
|
|
if (context.system && context.system->currentFactoryIndex() == target)
|
|
return;
|
|
context.requestAppSwitchByIndex(target);
|
|
}
|
|
|
|
void refreshEntries() {
|
|
entries.clear();
|
|
if (!context.system)
|
|
return;
|
|
const std::size_t total = context.system->appCount();
|
|
for (std::size_t i = 0; i < total; ++i) {
|
|
const cardboy::sdk::IAppFactory* factory = context.system->factoryAt(i);
|
|
if (!factory)
|
|
continue;
|
|
const char* name = factory->name();
|
|
if (!name)
|
|
continue;
|
|
if (std::string_view(name) == kMenuAppNameView)
|
|
continue;
|
|
entries.push_back(MenuEntry{std::string(name), i});
|
|
}
|
|
if (selected >= entries.size())
|
|
selected = entries.empty() ? 0 : entries.size() - 1;
|
|
dirty = true;
|
|
}
|
|
|
|
static void drawCenteredText(Framebuffer& fb, int y, std::string_view text, int scale, int letterSpacing = 0) {
|
|
const int width = font16x8::measureText(text, scale, letterSpacing);
|
|
const int x = (fb.width() - width) / 2;
|
|
font16x8::drawText(fb, x, y, text, scale, true, letterSpacing);
|
|
}
|
|
|
|
void drawPagerDots() {
|
|
if (entries.size() <= 1)
|
|
return;
|
|
const int count = static_cast<int>(entries.size());
|
|
const int spacing = 20;
|
|
const int dotSize = 7;
|
|
const int totalW = spacing * (count - 1);
|
|
const int startX = (framebuffer.width() - totalW) / 2;
|
|
const int baseline = framebuffer.height() - (font16x8::kGlyphHeight + 48);
|
|
for (int i = 0; i < count; ++i) {
|
|
const int cx = startX + i * spacing;
|
|
for (int dx = -dotSize / 2; dx <= dotSize / 2; ++dx) {
|
|
for (int dy = -dotSize / 2; dy <= dotSize / 2; ++dy) {
|
|
const bool isSelected = (static_cast<std::size_t>(i) == selected);
|
|
const bool on = isSelected || std::abs(dx) == dotSize / 2 || std::abs(dy) == dotSize / 2;
|
|
if (on)
|
|
framebuffer.drawPixel(cx + dx, baseline + dy, true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void renderIfNeeded() {
|
|
if (!dirty)
|
|
return;
|
|
dirty = false;
|
|
|
|
framebuffer.beginFrame();
|
|
framebuffer.clear(false);
|
|
|
|
drawCenteredText(framebuffer, 24, "APPS", 1, 1);
|
|
|
|
if (entries.empty()) {
|
|
drawCenteredText(framebuffer, framebuffer.height() / 2 - 18, "NO OTHER APPS", 2, 1);
|
|
drawCenteredText(framebuffer, framebuffer.height() - 72, "ADD MORE IN FIRMWARE", 1, 1);
|
|
} else {
|
|
const std::string& name = entries[selected].name;
|
|
const int titleScale = 2;
|
|
const int centerY = framebuffer.height() / 2 - (font16x8::kGlyphHeight * titleScale) / 2;
|
|
drawCenteredText(framebuffer, centerY, name, titleScale, 0);
|
|
|
|
const std::string indexLabel = std::to_string(selected + 1) + "/" + std::to_string(entries.size());
|
|
const int topRightX = framebuffer.width() - font16x8::measureText(indexLabel, 1, 0) - 16;
|
|
font16x8::drawText(framebuffer, topRightX, 20, indexLabel, 1, true, 0);
|
|
|
|
drawPagerDots();
|
|
drawCenteredText(framebuffer, framebuffer.height() - 48, "A/SELECT START", 1, 1);
|
|
drawCenteredText(framebuffer, framebuffer.height() - 28, "L/R CHOOSE", 1, 1);
|
|
}
|
|
|
|
framebuffer.endFrame();
|
|
}
|
|
};
|
|
|
|
class MenuAppFactory final : public cardboy::sdk::IAppFactory {
|
|
public:
|
|
const char* name() const override { return kMenuAppName; }
|
|
std::unique_ptr<cardboy::sdk::IApp> create(cardboy::sdk::AppContext& context) override {
|
|
return std::make_unique<MenuApp>(context);
|
|
}
|
|
};
|
|
|
|
} // namespace
|
|
|
|
std::unique_ptr<cardboy::sdk::IAppFactory> createMenuAppFactory() { return std::make_unique<MenuAppFactory>(); }
|
|
|
|
} // namespace apps
|