mirror of
https://github.com/usatiuk/cardboy.git
synced 2025-10-28 15:17:48 +01:00
statusbar
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
namespace cardboy::sdk {
|
||||
|
||||
class FramebufferHooks {
|
||||
public:
|
||||
using PreSendHook = void (*)(void* framebuffer, void* userData);
|
||||
|
||||
static void setPreSendHook(PreSendHook hook, void* userData);
|
||||
static void clearPreSendHook();
|
||||
static void invokePreSend(void* framebuffer);
|
||||
|
||||
private:
|
||||
static PreSendHook hook_;
|
||||
static void* userData_;
|
||||
};
|
||||
|
||||
} // namespace cardboy::sdk
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "input_state.hpp"
|
||||
#include "cardboy/sdk/framebuffer_hooks.hpp"
|
||||
|
||||
#include <concepts>
|
||||
#include <cstdint>
|
||||
@@ -67,9 +68,11 @@ public:
|
||||
}
|
||||
|
||||
__attribute__((always_inline)) void sendFrame(bool clearDrawBuffer = true) {
|
||||
if constexpr (detail::HasSendFrameImpl<Impl>)
|
||||
if constexpr (detail::HasSendFrameImpl<Impl>) {
|
||||
FramebufferHooks::invokePreSend(&impl());
|
||||
impl().sendFrame_impl(clearDrawBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] __attribute__((always_inline)) bool isFrameInFlight() const {
|
||||
if constexpr (detail::HasFrameInFlightImpl<Impl>)
|
||||
|
||||
@@ -2,6 +2,8 @@ cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
add_library(cardboy_sdk STATIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/app_system.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/status_bar.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/framebuffer_hooks.cpp
|
||||
)
|
||||
|
||||
set_target_properties(cardboy_sdk PROPERTIES
|
||||
|
||||
@@ -12,6 +12,7 @@ namespace cardboy::sdk {
|
||||
class AppSystem {
|
||||
public:
|
||||
explicit AppSystem(AppContext context);
|
||||
~AppSystem();
|
||||
|
||||
void registerApp(std::unique_ptr<IAppFactory> factory);
|
||||
bool startApp(const std::string& name);
|
||||
|
||||
20
Firmware/sdk/core/include/cardboy/sdk/framebuffer_hooks.hpp
Normal file
20
Firmware/sdk/core/include/cardboy/sdk/framebuffer_hooks.hpp
Normal file
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
namespace cardboy::sdk {
|
||||
|
||||
class FramebufferHooks {
|
||||
public:
|
||||
using PreSendHook = void (*)(void* framebuffer, void* userData);
|
||||
|
||||
static void setPreSendHook(PreSendHook hook, void* userData);
|
||||
static void clearPreSendHook();
|
||||
static void invokePreSend(void* framebuffer);
|
||||
|
||||
private:
|
||||
static PreSendHook hook_;
|
||||
static void* userData_;
|
||||
};
|
||||
|
||||
} // namespace cardboy::sdk
|
||||
83
Firmware/sdk/core/include/cardboy/sdk/status_bar.hpp
Normal file
83
Firmware/sdk/core/include/cardboy/sdk/status_bar.hpp
Normal file
@@ -0,0 +1,83 @@
|
||||
#pragma once
|
||||
|
||||
#include "cardboy/sdk/input_state.hpp"
|
||||
#include "cardboy/sdk/services.hpp"
|
||||
|
||||
#include "cardboy/gfx/font16x8.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace cardboy::sdk {
|
||||
|
||||
class StatusBar {
|
||||
public:
|
||||
static StatusBar& instance();
|
||||
|
||||
void setServices(Services* services) { services_ = services; }
|
||||
|
||||
void setEnabled(bool value);
|
||||
void toggle();
|
||||
[[nodiscard]] bool isEnabled() const { return enabled_; }
|
||||
|
||||
void setCurrentAppName(std::string_view name);
|
||||
|
||||
[[nodiscard]] bool handleToggleInput(const InputState& current, const InputState& previous);
|
||||
|
||||
template<typename Framebuffer>
|
||||
void renderIfEnabled(Framebuffer& fb) {
|
||||
if (!enabled_)
|
||||
return;
|
||||
renderBar(fb);
|
||||
}
|
||||
|
||||
private:
|
||||
StatusBar() = default;
|
||||
|
||||
template<typename Framebuffer>
|
||||
void renderBar(Framebuffer& fb) {
|
||||
const int width = fb.width();
|
||||
if (width <= 0)
|
||||
return;
|
||||
|
||||
const std::string leftText = prepareLeftText(width);
|
||||
const std::string rightText = prepareRightText();
|
||||
|
||||
for (int x = 0; x < width; ++x)
|
||||
fb.drawPixel(x, 0, true);
|
||||
|
||||
const int textY = 1;
|
||||
const int barHeight = font16x8::kGlyphHeight + 2;
|
||||
const int bottomSeparatorY = textY + font16x8::kGlyphHeight + 1;
|
||||
if (bottomSeparatorY < barHeight && bottomSeparatorY < fb.height()) {
|
||||
for (int x = 0; x < width; ++x)
|
||||
fb.drawPixel(x, bottomSeparatorY, (x % 2) == 0);
|
||||
}
|
||||
|
||||
const int leftX = 2;
|
||||
if (!leftText.empty())
|
||||
font16x8::drawText(fb, leftX, textY, leftText, 1, true, 1);
|
||||
|
||||
if (!rightText.empty()) {
|
||||
int rightWidth = font16x8::measureText(rightText, 1, 1);
|
||||
int rightX = width - rightWidth - 2;
|
||||
const int minRightX = leftX + font16x8::measureText(leftText, 1, 1) + 6;
|
||||
if (rightX < minRightX)
|
||||
rightX = std::max(minRightX, width / 2);
|
||||
if (rightX < width)
|
||||
font16x8::drawText(fb, rightX, textY, rightText, 1, true, 1);
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] std::string prepareLeftText(int displayWidth) const;
|
||||
[[nodiscard]] std::string prepareRightText() const;
|
||||
|
||||
bool enabled_ = false;
|
||||
Services* services_ = nullptr;
|
||||
std::string appName_{};
|
||||
};
|
||||
|
||||
} // namespace cardboy::sdk
|
||||
@@ -1,4 +1,6 @@
|
||||
#include "cardboy/sdk/app_system.hpp"
|
||||
#include "cardboy/sdk/framebuffer_hooks.hpp"
|
||||
#include "cardboy/sdk/status_bar.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
@@ -12,9 +14,25 @@ namespace {
|
||||
}
|
||||
|
||||
constexpr std::uint32_t kIdlePollMs = 16;
|
||||
|
||||
template<typename Framebuffer>
|
||||
void statusBarPreSendHook(void* framebuffer, void* userData) {
|
||||
auto* fb = static_cast<Framebuffer*>(framebuffer);
|
||||
auto* status = static_cast<StatusBar*>(userData);
|
||||
if (fb && status)
|
||||
status->renderIfEnabled(*fb);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
AppSystem::AppSystem(AppContext ctx) : context(std::move(ctx)) { context.system = this; }
|
||||
AppSystem::AppSystem(AppContext ctx) : context(std::move(ctx)) {
|
||||
context.system = this;
|
||||
auto& statusBar = StatusBar::instance();
|
||||
statusBar.setServices(context.services);
|
||||
using FBType = typename AppContext::Framebuffer;
|
||||
FramebufferHooks::setPreSendHook(&statusBarPreSendHook<FBType>, &statusBar);
|
||||
}
|
||||
|
||||
AppSystem::~AppSystem() { FramebufferHooks::clearPreSendHook(); }
|
||||
|
||||
void AppSystem::registerApp(std::unique_ptr<IAppFactory> factory) {
|
||||
if (!factory)
|
||||
@@ -53,6 +71,8 @@ bool AppSystem::startAppByIndex(std::size_t index) {
|
||||
clearTimersForCurrentApp();
|
||||
current = std::move(app);
|
||||
lastInputState = context.input.readState();
|
||||
StatusBar::instance().setServices(context.services);
|
||||
StatusBar::instance().setCurrentAppName(activeFactory ? activeFactory->name() : "");
|
||||
current->onStart();
|
||||
return true;
|
||||
}
|
||||
@@ -72,7 +92,9 @@ void AppSystem::run() {
|
||||
processDueTimers(now, events);
|
||||
|
||||
const InputState inputNow = context.input.readState();
|
||||
if (inputsDiffer(inputNow, lastInputState)) {
|
||||
const bool consumedByStatusToggle = StatusBar::instance().handleToggleInput(inputNow, lastInputState);
|
||||
|
||||
if (!consumedByStatusToggle && inputsDiffer(inputNow, lastInputState)) {
|
||||
AppEvent evt{};
|
||||
evt.type = AppEventType::Button;
|
||||
evt.timestamp_ms = now;
|
||||
@@ -80,6 +102,8 @@ void AppSystem::run() {
|
||||
evt.button.previous = lastInputState;
|
||||
events.push_back(evt);
|
||||
lastInputState = inputNow;
|
||||
} else if (consumedByStatusToggle) {
|
||||
lastInputState = inputNow;
|
||||
}
|
||||
|
||||
for (const auto& evt: events) {
|
||||
|
||||
23
Firmware/sdk/core/src/framebuffer_hooks.cpp
Normal file
23
Firmware/sdk/core/src/framebuffer_hooks.cpp
Normal file
@@ -0,0 +1,23 @@
|
||||
#include "cardboy/sdk/framebuffer_hooks.hpp"
|
||||
|
||||
namespace cardboy::sdk {
|
||||
|
||||
FramebufferHooks::PreSendHook FramebufferHooks::hook_ = nullptr;
|
||||
void* FramebufferHooks::userData_ = nullptr;
|
||||
|
||||
void FramebufferHooks::setPreSendHook(PreSendHook hook, void* userData) {
|
||||
hook_ = hook;
|
||||
userData_ = userData;
|
||||
}
|
||||
|
||||
void FramebufferHooks::clearPreSendHook() {
|
||||
hook_ = nullptr;
|
||||
userData_ = nullptr;
|
||||
}
|
||||
|
||||
void FramebufferHooks::invokePreSend(void* framebuffer) {
|
||||
if (hook_)
|
||||
hook_(framebuffer, userData_);
|
||||
}
|
||||
|
||||
} // namespace cardboy::sdk
|
||||
79
Firmware/sdk/core/src/status_bar.cpp
Normal file
79
Firmware/sdk/core/src/status_bar.cpp
Normal file
@@ -0,0 +1,79 @@
|
||||
#include "cardboy/sdk/status_bar.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstdio>
|
||||
|
||||
namespace cardboy::sdk {
|
||||
|
||||
StatusBar& StatusBar::instance() {
|
||||
static StatusBar bar;
|
||||
return bar;
|
||||
}
|
||||
|
||||
void StatusBar::setEnabled(bool value) { enabled_ = value; }
|
||||
|
||||
void StatusBar::toggle() {
|
||||
enabled_ = !enabled_;
|
||||
if (services_ && services_->buzzer)
|
||||
services_->buzzer->beepMove();
|
||||
}
|
||||
|
||||
void StatusBar::setCurrentAppName(std::string_view name) {
|
||||
appName_.assign(name.begin(), name.end());
|
||||
std::transform(appName_.begin(), appName_.end(), appName_.begin(), [](unsigned char ch) {
|
||||
return static_cast<char>(std::toupper(ch));
|
||||
});
|
||||
}
|
||||
|
||||
bool StatusBar::handleToggleInput(const InputState& current, const InputState& previous) {
|
||||
const bool comboNow = current.start && current.select && current.up;
|
||||
const bool comboPrev = previous.start && previous.select && previous.up;
|
||||
if (comboNow && !comboPrev) {
|
||||
toggle();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string StatusBar::prepareLeftText(int displayWidth) const {
|
||||
std::string text = appName_.empty() ? std::string("CARDBOY") : appName_;
|
||||
int maxWidth = std::max(0, displayWidth - 32);
|
||||
while (!text.empty() && font16x8::measureText(text, 1, 1) > maxWidth)
|
||||
text.pop_back();
|
||||
return text;
|
||||
}
|
||||
|
||||
std::string StatusBar::prepareRightText() const {
|
||||
if (!services_)
|
||||
return {};
|
||||
|
||||
std::string right;
|
||||
if (services_->battery && services_->battery->hasData()) {
|
||||
const float charge = services_->battery->charge();
|
||||
char buf[32];
|
||||
if (charge > 0.0f && charge <= 1.5f) {
|
||||
const int pct = std::clamp(static_cast<int>(charge * 100.0f + 0.5f), 0, 100);
|
||||
std::snprintf(buf, sizeof(buf), "BAT %d%%", pct);
|
||||
} else {
|
||||
std::snprintf(buf, sizeof(buf), "BAT %.2fV", static_cast<double>(services_->battery->voltage()));
|
||||
}
|
||||
right.assign(buf);
|
||||
}
|
||||
|
||||
if (services_->powerManager && services_->powerManager->isSlowMode()) {
|
||||
if (!right.empty())
|
||||
right.append(" ");
|
||||
right.append("SLOW");
|
||||
}
|
||||
|
||||
if (services_->buzzer && services_->buzzer->isMuted()) {
|
||||
if (!right.empty())
|
||||
right.append(" ");
|
||||
right.append("MUTE");
|
||||
}
|
||||
|
||||
return right;
|
||||
}
|
||||
|
||||
} // namespace cardboy::sdk
|
||||
Reference in New Issue
Block a user