Compare commits

...

4 Commits

Author SHA1 Message Date
2bb033a6ca some cleanup 2025-10-28 23:39:20 +01:00
4d0eb3d187 unicode fonts 2025-10-28 23:28:53 +01:00
5532055cdc snake fix 2025-10-25 23:42:01 +02:00
61f05b4e58 battery percentage fix with notifications 2025-10-25 23:41:56 +02:00
15 changed files with 798 additions and 5138 deletions

View File

@@ -80,6 +80,8 @@
"thread": "cpp",
"cinttypes": "cpp",
"typeinfo": "cpp",
"variant": "cpp"
"variant": "cpp",
"ranges": "cpp",
"shared_mutex": "cpp"
}
}

View File

@@ -447,15 +447,22 @@ private:
framebuffer.frameReady();
if (auto* battery = context.battery(); battery && battery->hasData()) {
const float percentage = battery->percentage();
if (std::isfinite(percentage) && percentage >= 0.0f) {
char pct[8];
std::snprintf(pct, sizeof(pct), "%.0f%%", static_cast<double>(percentage));
const int pctWidth = font16x8::measureText(pct, 1, 1);
const int pctX = framebuffer.width() - pctWidth - 4;
const int pctY = 4;
font16x8::drawText(framebuffer, pctX, pctY, pct, 1, true, 1);
const std::uint32_t nowMs = clock.millis();
const bool hasNotifications = !notifications.empty();
const bool showNotificationDetails =
hasNotifications && (nowMs - lastNotificationInteractionMs <= kNotificationHideMs);
if (!showNotificationDetails) {
if (auto* battery = context.battery(); battery && battery->hasData()) {
const float percentage = battery->percentage();
if (std::isfinite(percentage) && percentage >= 0.0f) {
char pct[8];
std::snprintf(pct, sizeof(pct), "%.0f%%", static_cast<double>(percentage));
const int pctWidth = font16x8::measureText(pct, 1, 1);
const int pctX = framebuffer.width() - pctWidth - 4;
const int pctY = 4;
font16x8::drawText(framebuffer, pctX, pctY, pct, 1, true, 1);
}
}
}
@@ -471,10 +478,6 @@ private:
int cardHeight = 0;
const int cardWidth = framebuffer.width() - cardMarginSide * 2;
const std::uint32_t nowMs = clock.millis();
const bool hasNotifications = !notifications.empty();
const bool showNotificationDetails =
hasNotifications && (nowMs - lastNotificationInteractionMs <= kNotificationHideMs);
if (hasNotifications) {
const auto& note = notifications[selectedNotification];

View File

@@ -18,9 +18,9 @@ namespace apps {
namespace {
using cardboy::sdk::AppButtonEvent;
using cardboy::sdk::AppTimeoutEvent;
using cardboy::sdk::AppContext;
using cardboy::sdk::AppEvent;
using cardboy::sdk::AppTimeoutEvent;
using cardboy::sdk::AppTimerEvent;
using Framebuffer = typename AppContext::Framebuffer;

View File

@@ -30,7 +30,7 @@ using cardboy::sdk::InputState;
constexpr char kSnakeAppName[] = "Snake";
constexpr int kBoardWidth = 32;
constexpr int kBoardHeight = 20;
constexpr int kBoardHeight = 18;
constexpr int kCellSize = 10;
constexpr int kInitialSnakeLength = 5;
constexpr int kScorePerFood = 10;

View File

@@ -40,232 +40,232 @@ public:
class DesktopBattery final : public cardboy::sdk::IBatteryMonitor {
public:
[[nodiscard]] bool hasData() const override { return false; }
};
class DesktopStorage final : public cardboy::sdk::IStorage {
public:
[[nodiscard]] bool readUint32(std::string_view ns, std::string_view key, std::uint32_t& out) override;
void writeUint32(std::string_view ns, std::string_view key, std::uint32_t value) override;
class DesktopStorage final : public cardboy::sdk::IStorage {
public:
[[nodiscard]] bool readUint32(std::string_view ns, std::string_view key, std::uint32_t& out) override;
void writeUint32(std::string_view ns, std::string_view key, std::uint32_t value) override;
private:
std::unordered_map<std::string, std::uint32_t> data;
private:
std::unordered_map<std::string, std::uint32_t> data;
static std::string composeKey(std::string_view ns, std::string_view key);
};
static std::string composeKey(std::string_view ns, std::string_view key);
class DesktopRandom final : public cardboy::sdk::IRandom {
public:
DesktopRandom();
[[nodiscard]] std::uint32_t nextUint32() override;
private:
std::mt19937 rng;
std::uniform_int_distribution<std::uint32_t> dist;
};
class DesktopHighResClock final : public cardboy::sdk::IHighResClock {
public:
DesktopHighResClock();
[[nodiscard]] std::uint64_t micros() override;
private:
const std::chrono::steady_clock::time_point start;
};
class DesktopFilesystem final : public cardboy::sdk::IFilesystem {
public:
DesktopFilesystem();
bool mount() override;
[[nodiscard]] bool isMounted() const override { return mounted; }
[[nodiscard]] std::string basePath() const override { return basePathPath.string(); }
private:
std::filesystem::path basePathPath;
bool mounted = false;
};
class DesktopNotificationCenter final : public cardboy::sdk::INotificationCenter {
public:
void pushNotification(Notification notification) override;
[[nodiscard]] std::uint32_t revision() const override;
[[nodiscard]] std::vector<Notification> recent(std::size_t limit) const override;
void markAllRead() override;
void clear() override;
void removeById(std::uint64_t id) override;
void removeByExternalId(std::uint64_t externalId) override;
private:
static constexpr std::size_t kMaxEntries = 8;
mutable std::mutex mutex;
std::vector<Notification> entries;
std::uint64_t nextId = 1;
std::uint32_t revisionCounter = 0;
};
class DesktopLoopHooks final : public cardboy::sdk::ILoopHooks {
public:
explicit DesktopLoopHooks(DesktopRuntime& owner);
void onLoopIteration() override;
private:
DesktopRuntime& runtime;
};
class DesktopEventBus final : public cardboy::sdk::IEventBus {
public:
void post(const cardboy::sdk::AppEvent& event) override;
std::optional<cardboy::sdk::AppEvent> pop(std::optional<std::uint32_t> timeout_ms = std::nullopt) override;
private:
std::mutex mutex;
std::condition_variable cv;
std::deque<cardboy::sdk::AppEvent> queue;
};
class DesktopTimerService final : public cardboy::sdk::ITimerService {
public:
DesktopTimerService(DesktopRuntime& owner, cardboy::sdk::IEventBus& eventBus);
~DesktopTimerService() override;
cardboy::sdk::AppTimerHandle scheduleTimer(std::uint32_t delay_ms, bool repeat) override;
void cancelTimer(cardboy::sdk::AppTimerHandle handle) override;
void cancelAllTimers() override;
private:
struct TimerRecord {
cardboy::sdk::AppTimerHandle handle = cardboy::sdk::kInvalidAppTimer;
std::chrono::steady_clock::time_point due;
std::chrono::milliseconds interval{0};
bool repeat = false;
bool active = true;
};
class DesktopRandom final : public cardboy::sdk::IRandom {
public:
DesktopRandom();
void workerLoop();
void wakeWorker();
void cleanupInactive();
[[nodiscard]] std::uint32_t nextUint32() override;
DesktopRuntime& runtime;
cardboy::sdk::IEventBus& eventBus;
std::mutex mutex;
std::condition_variable cv;
std::vector<TimerRecord> timers;
bool stopWorker = false;
std::thread worker;
cardboy::sdk::AppTimerHandle nextHandle = 1;
};
private:
std::mt19937 rng;
std::uniform_int_distribution<std::uint32_t> dist;
class DesktopAppServiceProvider final : public cardboy::sdk::IAppServiceProvider {
public:
DesktopAppServiceProvider(DesktopRuntime& owner, cardboy::sdk::IEventBus& bus);
[[nodiscard]] std::unique_ptr<cardboy::sdk::AppScopedServices>
createScopedServices(std::uint64_t generation) override;
private:
struct ScopedServices final : cardboy::sdk::AppScopedServices {
std::unique_ptr<DesktopTimerService> ownedTimer;
};
class DesktopHighResClock final : public cardboy::sdk::IHighResClock {
public:
DesktopHighResClock();
DesktopRuntime& runtime;
cardboy::sdk::IEventBus& eventBus;
};
[[nodiscard]] std::uint64_t micros() override;
class DesktopFramebuffer final : public cardboy::sdk::FramebufferFacade<DesktopFramebuffer> {
public:
explicit DesktopFramebuffer(DesktopRuntime& runtime);
private:
const std::chrono::steady_clock::time_point start;
};
[[nodiscard]] int width_impl() const;
[[nodiscard]] int height_impl() const;
void drawPixel_impl(int x, int y, bool on);
void clear_impl(bool on);
void frameReady_impl();
void sendFrame_impl(bool clearAfterSend);
[[nodiscard]] bool frameInFlight_impl() const { return false; }
class DesktopFilesystem final : public cardboy::sdk::IFilesystem {
public:
DesktopFilesystem();
private:
DesktopRuntime& runtime;
};
bool mount() override;
[[nodiscard]] bool isMounted() const override { return mounted; }
[[nodiscard]] std::string basePath() const override { return basePathPath.string(); }
class DesktopInput final : public cardboy::sdk::InputFacade<DesktopInput> {
public:
explicit DesktopInput(DesktopRuntime& runtime);
private:
std::filesystem::path basePathPath;
bool mounted = false;
};
cardboy::sdk::InputState readState_impl();
void handleKey(sf::Keyboard::Key key, bool pressed);
class DesktopNotificationCenter final : public cardboy::sdk::INotificationCenter {
public:
void pushNotification(Notification notification) override;
[[nodiscard]] std::uint32_t revision() const override;
[[nodiscard]] std::vector<Notification> recent(std::size_t limit) const override;
void markAllRead() override;
void clear() override;
void removeById(std::uint64_t id) override;
void removeByExternalId(std::uint64_t externalId) override;
private:
DesktopRuntime& runtime;
cardboy::sdk::InputState state{};
};
private:
static constexpr std::size_t kMaxEntries = 8;
class DesktopClock final : public cardboy::sdk::ClockFacade<DesktopClock> {
public:
explicit DesktopClock(DesktopRuntime& runtime);
mutable std::mutex mutex;
std::vector<Notification> entries;
std::uint64_t nextId = 1;
std::uint32_t revisionCounter = 0;
};
std::uint32_t millis_impl();
void sleep_ms_impl(std::uint32_t ms);
class DesktopLoopHooks final : public cardboy::sdk::ILoopHooks {
public:
explicit DesktopLoopHooks(DesktopRuntime& owner);
private:
DesktopRuntime& runtime;
const std::chrono::steady_clock::time_point start;
};
void onLoopIteration() override;
class DesktopRuntime {
public:
DesktopRuntime();
private:
DesktopRuntime& runtime;
};
cardboy::sdk::Services& serviceRegistry();
void processEvents();
void presentIfNeeded();
void sleepFor(std::uint32_t ms);
class DesktopEventBus final : public cardboy::sdk::IEventBus {
public:
void post(const cardboy::sdk::AppEvent& event) override;
std::optional<cardboy::sdk::AppEvent> pop(std::optional<std::uint32_t> timeout_ms = std::nullopt) override;
[[nodiscard]] bool isRunning() const { return running; }
private:
std::mutex mutex;
std::condition_variable cv;
std::deque<cardboy::sdk::AppEvent> queue;
};
DesktopFramebuffer framebuffer;
DesktopInput input;
DesktopClock clock;
class DesktopTimerService final : public cardboy::sdk::ITimerService {
public:
DesktopTimerService(DesktopRuntime& owner, cardboy::sdk::IEventBus& eventBus);
~DesktopTimerService() override;
private:
friend class DesktopFramebuffer;
friend class DesktopInput;
friend class DesktopClock;
cardboy::sdk::AppTimerHandle scheduleTimer(std::uint32_t delay_ms, bool repeat) override;
void cancelTimer(cardboy::sdk::AppTimerHandle handle) override;
void cancelAllTimers() override;
void setPixel(int x, int y, bool on);
void clearPixels(bool on);
private:
struct TimerRecord {
cardboy::sdk::AppTimerHandle handle = cardboy::sdk::kInvalidAppTimer;
std::chrono::steady_clock::time_point due;
std::chrono::milliseconds interval{0};
bool repeat = false;
bool active = true;
};
sf::RenderWindow window;
sf::Texture texture;
sf::Sprite sprite;
std::vector<std::uint8_t> pixels;
bool dirty = true;
bool running = true;
bool clearNextFrame = true;
void workerLoop();
void wakeWorker();
void cleanupInactive();
DesktopBuzzer buzzerService;
DesktopBattery batteryService;
DesktopStorage storageService;
DesktopRandom randomService;
DesktopHighResClock highResService;
DesktopFilesystem filesystemService;
DesktopEventBus eventBusService;
DesktopAppServiceProvider appServiceProvider;
DesktopNotificationCenter notificationService;
DesktopLoopHooks loopHooksService;
cardboy::sdk::Services services{};
};
DesktopRuntime& runtime;
cardboy::sdk::IEventBus& eventBus;
std::mutex mutex;
std::condition_variable cv;
std::vector<TimerRecord> timers;
bool stopWorker = false;
std::thread worker;
cardboy::sdk::AppTimerHandle nextHandle = 1;
};
struct Backend {
using Framebuffer = DesktopFramebuffer;
using Input = DesktopInput;
using Clock = DesktopClock;
};
class DesktopAppServiceProvider final : public cardboy::sdk::IAppServiceProvider {
public:
DesktopAppServiceProvider(DesktopRuntime& owner, cardboy::sdk::IEventBus& bus);
[[nodiscard]] std::unique_ptr<cardboy::sdk::AppScopedServices>
createScopedServices(std::uint64_t generation) override;
private:
struct ScopedServices final : cardboy::sdk::AppScopedServices {
std::unique_ptr<DesktopTimerService> ownedTimer;
};
DesktopRuntime& runtime;
cardboy::sdk::IEventBus& eventBus;
};
class DesktopFramebuffer final : public cardboy::sdk::FramebufferFacade<DesktopFramebuffer> {
public:
explicit DesktopFramebuffer(DesktopRuntime& runtime);
[[nodiscard]] int width_impl() const;
[[nodiscard]] int height_impl() const;
void drawPixel_impl(int x, int y, bool on);
void clear_impl(bool on);
void frameReady_impl();
void sendFrame_impl(bool clearAfterSend);
[[nodiscard]] bool frameInFlight_impl() const { return false; }
private:
DesktopRuntime& runtime;
};
class DesktopInput final : public cardboy::sdk::InputFacade<DesktopInput> {
public:
explicit DesktopInput(DesktopRuntime& runtime);
cardboy::sdk::InputState readState_impl();
void handleKey(sf::Keyboard::Key key, bool pressed);
private:
DesktopRuntime& runtime;
cardboy::sdk::InputState state{};
};
class DesktopClock final : public cardboy::sdk::ClockFacade<DesktopClock> {
public:
explicit DesktopClock(DesktopRuntime& runtime);
std::uint32_t millis_impl();
void sleep_ms_impl(std::uint32_t ms);
private:
DesktopRuntime& runtime;
const std::chrono::steady_clock::time_point start;
};
class DesktopRuntime {
public:
DesktopRuntime();
cardboy::sdk::Services& serviceRegistry();
void processEvents();
void presentIfNeeded();
void sleepFor(std::uint32_t ms);
[[nodiscard]] bool isRunning() const { return running; }
DesktopFramebuffer framebuffer;
DesktopInput input;
DesktopClock clock;
private:
friend class DesktopFramebuffer;
friend class DesktopInput;
friend class DesktopClock;
void setPixel(int x, int y, bool on);
void clearPixels(bool on);
sf::RenderWindow window;
sf::Texture texture;
sf::Sprite sprite;
std::vector<std::uint8_t> pixels;
bool dirty = true;
bool running = true;
bool clearNextFrame = true;
DesktopBuzzer buzzerService;
DesktopBattery batteryService;
DesktopStorage storageService;
DesktopRandom randomService;
DesktopHighResClock highResService;
DesktopFilesystem filesystemService;
DesktopEventBus eventBusService;
DesktopAppServiceProvider appServiceProvider;
DesktopNotificationCenter notificationService;
DesktopLoopHooks loopHooksService;
cardboy::sdk::Services services{};
};
struct Backend {
using Framebuffer = DesktopFramebuffer;
using Input = DesktopInput;
using Clock = DesktopClock;
};
} // namespace cardboy::backend::desktop
}; // namespace cardboy::backend::desktop
namespace cardboy::backend {
using DesktopBackend = desktop::Backend;
using DesktopBackend = desktop::Backend;
} // namespace cardboy::backend

View File

@@ -1,25 +1,67 @@
cmake_minimum_required(VERSION 3.16)
include(FetchContent)
FetchContent_Declare(uni_algo
URL https://github.com/uni-algo/uni-algo/archive/refs/tags/v1.2.0.tar.gz
URL_HASH SHA256=f2a1539cd8635bc6088d05144a73ecfe7b4d74ee0361fabed6f87f9f19e74ca9
)
FetchContent_MakeAvailable(uni_algo)
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
${CMAKE_CURRENT_SOURCE_DIR}/src/persistent_settings.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/font_repository.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/psf_font.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/builtin_fonts.cpp
)
set_target_properties(cardboy_sdk PROPERTIES
EXPORT_NAME sdk
)
target_include_directories(cardboy_sdk
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/include
)
target_compile_features(cardboy_sdk PUBLIC cxx_std_20)
target_link_libraries(cardboy_sdk
PUBLIC
cardboy_backend_interface
${CARDBOY_SDK_BACKEND_LIBRARY}
uni-algo::uni-algo
)
target_include_directories(cardboy_sdk
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/include
)
set(PSF_FONT_FILES
${CMAKE_CURRENT_SOURCE_DIR}/assets/fonts/spleen8x16.psf
)
find_program(XXD_EXECUTABLE xxd)
if (NOT XXD_EXECUTABLE)
message(FATAL_ERROR "xxd not found; required to embed PSF fonts")
endif()
set(_cardboy_font_generated_sources "")
foreach(psf_file IN LISTS PSF_FONT_FILES)
get_filename_component(psf_name "${psf_file}" NAME)
get_filename_component(psf_stem "${psf_file}" NAME_WE)
set(psf_symbol "${psf_stem}_psf")
set(out_cpp "${CMAKE_CURRENT_BINARY_DIR}/${psf_name}.cpp")
add_custom_command(
OUTPUT "${out_cpp}"
COMMAND ${XXD_EXECUTABLE} -i -n ${psf_symbol} "${psf_file}" "${out_cpp}"
DEPENDS "${psf_file}"
COMMENT "Embedding PSF font ${psf_name}"
)
set_source_files_properties("${out_cpp}" PROPERTIES GENERATED TRUE)
list(APPEND _cardboy_font_generated_sources "${out_cpp}")
endforeach()
if (_cardboy_font_generated_sources)
target_sources(cardboy_sdk PRIVATE ${_cardboy_font_generated_sources})
endif()

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,8 @@
#pragma once
namespace cardboy::gfx::builtin {
void ensureRegistered();
} // namespace cardboy::gfx::builtin

View File

@@ -1,28 +1,21 @@
#pragma once
#include "cardboy/gfx/Fonts.hpp"
#include "cardboy/gfx/builtin_fonts.hpp"
#include "cardboy/gfx/font_repository.hpp"
#include <array>
#include <cctype>
#include <cstddef>
#include <cstdint>
#include <string_view>
#include <uni_algo/ranges.h>
#include "uni_algo/ranges_conv.h"
namespace font16x8 {
constexpr int kGlyphWidth = 8;
constexpr int kGlyphHeight = 16;
constexpr unsigned char kFallbackChar = '?';
inline unsigned char normalizeChar(char ch) {
unsigned char uc = static_cast<unsigned char>(ch);
if (!std::isprint(static_cast<unsigned char>(uc)))
return kFallbackChar;
return uc;
}
inline const std::array<uint8_t, kGlyphHeight>& glyphBitmap(char ch) {
unsigned char uc = normalizeChar(ch);
return fonts_Terminess_Powerline[uc];
}
constexpr int kGlyphWidth = 8;
constexpr int kGlyphHeight = 16;
constexpr std::uint32_t kFallbackCodepoint = static_cast<std::uint32_t>('?');
enum class Rotation {
None,
@@ -35,22 +28,86 @@ struct TextBounds {
int height = 0;
};
namespace detail {
inline const cardboy::gfx::FontRecord* defaultFontRecord() {
using cardboy::gfx::FontRepository;
using cardboy::gfx::builtin::ensureRegistered;
static const cardboy::gfx::FontRecord* record = []() -> const cardboy::gfx::FontRecord* {
ensureRegistered();
auto& repo = FontRepository::instance();
if (const auto* spleen = repo.getFont("Spleen 8x16"))
return spleen;
return repo.getFont("Terminess 8x16");
}();
return record;
}
inline const cardboy::gfx::ParsedPsfFont* defaultFont() {
const auto* record = defaultFontRecord();
if (!record)
return nullptr;
if (!record->font.valid)
return nullptr;
return &record->font;
}
inline std::uint32_t bytesPerRow(const cardboy::gfx::ParsedPsfFont& font) { return (font.view.width + 7u) / 8u; }
inline std::size_t countCodepoints(std::string_view text) {
std::size_t count = 0;
for (auto cp: una::ranges::utf8_view{text}) {
(void) cp;
++count;
}
return count;
}
inline int fontGlyphWidth() {
const auto* font = defaultFont();
if (!font || font->view.width == 0)
return kGlyphWidth;
return static_cast<int>(font->view.width);
}
inline int fontGlyphHeight() {
const auto* font = defaultFont();
if (!font || font->view.height == 0)
return kGlyphHeight;
return static_cast<int>(font->view.height);
}
} // namespace detail
template<typename Framebuffer>
inline void drawGlyph(Framebuffer& fb, int x, int y, char ch, int scale = 1, bool on = true,
Rotation rotation = Rotation::None) {
const auto& rows = glyphBitmap(ch);
if (rotation == Rotation::None && scale == 1 && on && ((x % 8) == 0)) {
for (int row = 0; row < kGlyphHeight; ++row) {
const uint8_t rowBits = rows[row];
inline void drawGlyphCodepoint(Framebuffer& fb, int x, int y, std::uint32_t codepoint, int scale = 1, bool on = true,
Rotation rotation = Rotation::None) {
const auto* font = detail::defaultFont();
if (!font)
return;
const auto glyphIndex = cardboy::gfx::glyphIndexForCodepoint(*font, codepoint);
const auto* glyph = font->view.glyphPointer(glyphIndex);
if (!glyph)
return;
const int width = detail::fontGlyphWidth();
const int height = detail::fontGlyphHeight();
const std::uint32_t rowStride = detail::bytesPerRow(*font);
if (rotation == Rotation::None && scale == 1 && on && rowStride == 1 && ((x % 8) == 0)) {
for (int row = 0; row < height; ++row) {
const std::uint8_t rowBits = glyph[row];
fb.drawBits8(x, y + row, rowBits);
}
return;
}
for (int row = 0; row < kGlyphHeight; ++row) {
const uint8_t rowBits = rows[row];
for (int col = 0; col < kGlyphWidth; ++col) {
const uint8_t mask = static_cast<uint8_t>(1u << (kGlyphWidth - 1 - col));
if ((rowBits & mask) == 0)
for (int row = 0; row < height; ++row) {
const std::uint8_t* rowData = glyph + row * rowStride;
for (int col = 0; col < width; ++col) {
const std::uint8_t byte = rowData[col / 8];
const std::uint8_t mask = static_cast<std::uint8_t>(0x80u >> (col % 8));
if ((byte & mask) == 0)
continue;
for (int sx = 0; sx < scale; ++sx) {
for (int sy = 0; sy < scale; ++sy) {
@@ -63,10 +120,10 @@ inline void drawGlyph(Framebuffer& fb, int x, int y, char ch, int scale = 1, boo
break;
case Rotation::Clockwise90:
dstX += row * scale + sx;
dstY += (kGlyphWidth - 1 - col) * scale + sy;
dstY += (width - 1 - col) * scale + sy;
break;
case Rotation::CounterClockwise90:
dstX += (kGlyphHeight - 1 - row) * scale + sx;
dstX += (height - 1 - row) * scale + sx;
dstY += col * scale + sy;
break;
}
@@ -77,13 +134,25 @@ inline void drawGlyph(Framebuffer& fb, int x, int y, char ch, int scale = 1, boo
}
}
template<typename Framebuffer>
inline void drawGlyph(Framebuffer& fb, int x, int y, char ch, int scale = 1, bool on = true,
Rotation rotation = Rotation::None) {
const auto codepoint = static_cast<std::uint32_t>(static_cast<unsigned char>(ch));
drawGlyphCodepoint(fb, x, y, codepoint, scale, on, rotation);
}
inline TextBounds measureTextBounds(std::string_view text, int scale = 1, int letterSpacing = 1,
Rotation rotation = Rotation::None) {
if (text.empty())
return {};
const int advance = (kGlyphWidth + letterSpacing) * scale;
const int extent = static_cast<int>(text.size()) * advance - letterSpacing * scale;
const int height = kGlyphHeight * scale;
const std::size_t glyphCount = detail::countCodepoints(text);
if (glyphCount == 0)
return {};
const int glyphWidth = detail::fontGlyphWidth();
const int glyphHeight = detail::fontGlyphHeight();
const int advance = (glyphWidth + letterSpacing) * scale;
const int extent = static_cast<int>(glyphCount) * advance - letterSpacing * scale;
const int height = glyphHeight * scale;
switch (rotation) {
case Rotation::None:
return {extent, height};
@@ -97,8 +166,12 @@ inline TextBounds measureTextBounds(std::string_view text, int scale = 1, int le
inline int measureText(std::string_view text, int scale = 1, int letterSpacing = 1) {
if (text.empty())
return 0;
const int advance = (kGlyphWidth + letterSpacing) * scale;
return static_cast<int>(text.size()) * advance - letterSpacing * scale;
const std::size_t glyphCount = detail::countCodepoints(text);
if (glyphCount == 0)
return 0;
const int glyphWidth = detail::fontGlyphWidth();
const int advance = (glyphWidth + letterSpacing) * scale;
return static_cast<int>(glyphCount) * advance - letterSpacing * scale;
}
template<typename Framebuffer>
@@ -106,17 +179,21 @@ inline void drawText(Framebuffer& fb, int x, int y, std::string_view text, int s
int letterSpacing = 1, Rotation rotation = Rotation::None) {
if (text.empty())
return;
const int advance = (kGlyphWidth + letterSpacing) * scale;
const int glyphWidth = detail::fontGlyphWidth();
const int advance = (glyphWidth + letterSpacing) * scale;
if (rotation == Rotation::None) {
int cursor = x;
for (char ch: text) {
drawGlyph(fb, cursor, y, ch, scale, on, rotation);
for (auto cp: una::ranges::utf8_view{text}) {
const auto codepoint = static_cast<std::uint32_t>(cp);
drawGlyphCodepoint(fb, cursor, y, codepoint, scale, on, rotation);
cursor += advance;
}
} else {
int cursor = y;
for (char ch: text) {
drawGlyph(fb, x, cursor, ch, scale, on, rotation);
for (auto cp: una::ranges::utf8_view{text}) {
const auto codepoint = static_cast<std::uint32_t>(cp);
drawGlyphCodepoint(fb, x, cursor, codepoint, scale, on, rotation);
cursor += advance;
}
}

View File

@@ -0,0 +1,50 @@
#pragma once
#include "cardboy/gfx/psf_font.hpp"
#include <cstddef>
#include <cstdint>
#include <memory>
#include <mutex>
#include <string>
#include <string_view>
#include <vector>
#include <span>
namespace cardboy::gfx {
struct FontInfo {
std::string_view name;
const std::uint8_t* data;
std::size_t size;
};
struct FontRecord {
std::string name;
const std::uint8_t* data = nullptr;
std::size_t size = 0;
ParsedPsfFont font;
};
class FontRepository {
public:
static FontRepository& instance();
const FontRecord* addFont(std::string_view name, const std::uint8_t* data, std::size_t size);
const FontRecord* getFont(std::string_view name) const;
std::vector<FontInfo> listFonts() const;
private:
FontRepository() = default;
~FontRepository() = default;
FontRepository(const FontRepository&) = delete;
FontRepository& operator=(const FontRepository&) = delete;
const FontRecord* addOrUpdateLocked(std::string_view name, const std::uint8_t* data, std::size_t size);
mutable std::mutex mutex_;
std::vector<std::unique_ptr<FontRecord>> fonts_;
};
} // namespace cardboy::gfx

View File

@@ -0,0 +1,48 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <span>
#include <string>
#include <string_view>
#include <unordered_map>
#include <vector>
namespace cardboy::gfx {
enum class PsfFormat { Unknown, Psf1, Psf2 };
struct PsfFontView {
PsfFormat format = PsfFormat::Unknown;
std::span<const std::uint8_t> blob = {};
std::span<const std::uint8_t> glyphs = {};
std::span<const std::uint8_t> unicode = {};
std::uint32_t glyphCount = 0;
std::uint32_t bytesPerGlyph = 0;
std::uint32_t width = 0;
std::uint32_t height = 0;
bool hasUnicode = false;
const std::uint8_t* glyphPointer(std::uint32_t index) const;
};
struct PsfUnicodeEntry {
std::uint32_t codepoint = 0;
std::uint32_t glyphIndex = 0;
};
struct ParsedPsfFont {
PsfFontView view;
std::vector<PsfUnicodeEntry> unicodeEntries;
std::unordered_map<std::uint32_t, std::uint32_t> unicodeToGlyph;
std::uint32_t defaultGlyphIndex = 0;
bool valid = false;
std::string error;
};
ParsedPsfFont parsePsfFont(std::span<const std::uint8_t> blob);
std::uint32_t glyphIndexForCodepoint(const ParsedPsfFont& font, std::uint32_t codepoint);
} // namespace cardboy::gfx

View File

@@ -0,0 +1,31 @@
#include "cardboy/gfx/builtin_fonts.hpp"
#include "cardboy/gfx/font_repository.hpp"
#include <cstddef>
#include <cstdint>
extern unsigned char spleen8x16_psf[];
extern unsigned int spleen8x16_psf_len;
namespace cardboy::gfx::builtin {
namespace {
void registerFonts() {
static bool registered = false;
if (registered)
return;
registered = true;
const auto* spleenData = reinterpret_cast<const std::uint8_t*>(spleen8x16_psf);
const std::size_t spleenSize = static_cast<std::size_t>(spleen8x16_psf_len);
FontRepository::instance().addFont("Spleen 8x16", spleenData, spleenSize);
}
} // namespace
void ensureRegistered() {
registerFonts();
}
} // namespace cardboy::gfx::builtin

View File

@@ -0,0 +1,66 @@
#include "cardboy/gfx/font_repository.hpp"
#include <algorithm>
namespace cardboy::gfx {
FontRepository& FontRepository::instance() {
static FontRepository repository;
return repository;
}
const FontRecord* FontRepository::addFont(std::string_view name, const std::uint8_t* data, std::size_t size) {
if (name.empty() || data == nullptr || size == 0)
return nullptr;
std::lock_guard<std::mutex> lock(mutex_);
return addOrUpdateLocked(name, data, size);
}
const FontRecord* FontRepository::getFont(std::string_view name) const {
std::lock_guard<std::mutex> lock(mutex_);
const auto it = std::find_if(fonts_.begin(), fonts_.end(),
[&](const std::unique_ptr<FontRecord>& record) { return record->name == name; });
if (it == fonts_.end())
return nullptr;
return it->get();
}
std::vector<FontInfo> FontRepository::listFonts() const {
std::lock_guard<std::mutex> lock(mutex_);
std::vector<FontInfo> result;
result.reserve(fonts_.size());
for (const auto& record: fonts_) {
result.push_back(FontInfo{
.name = record->name,
.data = record->data,
.size = record->size,
});
}
return result;
}
const FontRecord* FontRepository::addOrUpdateLocked(std::string_view name,
const std::uint8_t* data,
std::size_t size) {
auto it = std::find_if(fonts_.begin(), fonts_.end(),
[&](const std::unique_ptr<FontRecord>& record) { return record->name == name; });
if (it == fonts_.end()) {
auto record = std::make_unique<FontRecord>();
record->name = std::string{name};
record->data = data;
record->size = size;
record->font = parsePsfFont(std::span<const std::uint8_t>(data, size));
fonts_.push_back(std::move(record));
it = std::prev(fonts_.end());
} else {
(*it)->data = data;
(*it)->size = size;
(*it)->font = parsePsfFont(std::span<const std::uint8_t>(data, size));
}
if (!(*it)->font.valid) {
(*it)->font.error = "Failed to parse PSF font";
}
return it->get();
}
} // namespace cardboy::gfx

View File

@@ -0,0 +1,211 @@
#include "cardboy/gfx/psf_font.hpp"
#include <algorithm>
#include <cstring>
#include <iterator>
namespace cardboy::gfx {
namespace {
constexpr std::uint8_t kPsf1Magic0 = 0x36;
constexpr std::uint8_t kPsf1Magic1 = 0x04;
constexpr std::uint8_t kPsf1Mode512 = 0x01;
constexpr std::uint8_t kPsf1ModeHasTab = 0x02;
constexpr std::uint16_t kPsf1Separator = 0xFFFF;
constexpr std::uint16_t kPsf1StartSeq = 0xFFFE;
constexpr std::uint32_t kPsf2Magic = 0x864ab572;
constexpr std::uint8_t kPsf2Separator = 0xFF;
constexpr std::uint8_t kPsf2StartSeq = 0xFE;
struct Psf1Header {
std::uint8_t magic[2];
std::uint8_t mode;
std::uint8_t charsize;
};
struct Psf2Header {
std::uint32_t magic;
std::uint32_t version;
std::uint32_t headerSize;
std::uint32_t flags;
std::uint32_t length;
std::uint32_t charSize;
std::uint32_t height;
std::uint32_t width;
};
template<typename It>
std::uint32_t decodeUtf8(It& it, const It end) {
if (it == end)
return 0xFFFFFFFFu;
const std::uint8_t first = *it++;
if ((first & 0x80u) == 0)
return first;
if ((first & 0xE0u) == 0xC0u) {
if (it == end)
return 0xFFFFFFFFu;
const std::uint8_t b1 = *it++;
return ((first & 0x1Fu) << 6u) | (b1 & 0x3Fu);
}
if ((first & 0xF0u) == 0xE0u) {
if (std::distance(it, end) < 2)
return 0xFFFFFFFFu;
const std::uint8_t b1 = *it++;
const std::uint8_t b2 = *it++;
return ((first & 0x0Fu) << 12u) | ((b1 & 0x3Fu) << 6u) | (b2 & 0x3Fu);
}
if ((first & 0xF8u) == 0xF0u) {
if (std::distance(it, end) < 3)
return 0xFFFFFFFFu;
const std::uint8_t b1 = *it++;
const std::uint8_t b2 = *it++;
const std::uint8_t b3 = *it++;
return ((first & 0x07u) << 18u) | ((b1 & 0x3Fu) << 12u) | ((b2 & 0x3Fu) << 6u) | (b3 & 0x3Fu);
}
return 0xFFFFFFFFu;
}
} // namespace
const std::uint8_t* PsfFontView::glyphPointer(std::uint32_t index) const {
if (glyphs.empty() || index >= glyphCount)
return nullptr;
const std::size_t offset = static_cast<std::size_t>(index) * bytesPerGlyph;
if (offset + bytesPerGlyph > glyphs.size())
return nullptr;
return glyphs.data() + offset;
}
ParsedPsfFont parsePsfFont(std::span<const std::uint8_t> blob) {
ParsedPsfFont parsed;
parsed.view.blob = blob;
if (blob.size() < sizeof(Psf1Header))
return parsed;
const auto* psf1 = reinterpret_cast<const Psf1Header*>(blob.data());
if (psf1->magic[0] == kPsf1Magic0 && psf1->magic[1] == kPsf1Magic1) {
parsed.view.format = PsfFormat::Psf1;
const std::uint32_t count = (psf1->mode & kPsf1Mode512) ? 512u : 256u;
const std::uint32_t glyphBytes = static_cast<std::uint32_t>(psf1->charsize);
if (blob.size() < sizeof(Psf1Header) + static_cast<std::size_t>(count) * glyphBytes)
return parsed;
parsed.view.glyphCount = count;
parsed.view.bytesPerGlyph = glyphBytes;
parsed.view.width = 8;
parsed.view.height = glyphBytes;
parsed.view.glyphs = blob.subspan(sizeof(Psf1Header), static_cast<std::size_t>(count) * glyphBytes);
parsed.view.hasUnicode = (psf1->mode & kPsf1ModeHasTab) != 0;
if (parsed.view.hasUnicode && parsed.view.glyphs.size() < blob.size())
parsed.view.unicode = blob.subspan(sizeof(Psf1Header) + parsed.view.glyphs.size());
} else if (blob.size() >= sizeof(Psf2Header)) {
const auto* psf2 = reinterpret_cast<const Psf2Header*>(blob.data());
if (psf2->magic != kPsf2Magic)
return parsed;
parsed.view.format = PsfFormat::Psf2;
parsed.view.glyphCount = psf2->length;
parsed.view.bytesPerGlyph = psf2->charSize;
parsed.view.width = psf2->width;
parsed.view.height = psf2->height;
const std::size_t glyphOffset = psf2->headerSize;
const std::size_t glyphBytes = static_cast<std::size_t>(psf2->length) * psf2->charSize;
if (blob.size() < glyphOffset + glyphBytes)
return parsed;
parsed.view.glyphs = blob.subspan(glyphOffset, glyphBytes);
parsed.view.hasUnicode = (psf2->flags & 0x01u) != 0;
if (parsed.view.hasUnicode && glyphOffset + glyphBytes < blob.size())
parsed.view.unicode = blob.subspan(glyphOffset + glyphBytes);
} else {
return parsed;
}
parsed.valid = true;
// Build Unicode mapping if available.
if (parsed.view.hasUnicode && !parsed.view.unicode.empty()) {
parsed.unicodeEntries.reserve(parsed.view.glyphCount);
parsed.unicodeToGlyph.reserve(parsed.view.glyphCount);
if (parsed.view.format == PsfFormat::Psf1) {
const auto* data = parsed.view.unicode.data();
std::size_t remaining = parsed.view.unicode.size();
for (std::uint32_t glyph = 0; glyph < parsed.view.glyphCount && remaining >= sizeof(std::uint16_t); ++glyph) {
bool inSequence = false;
while (remaining >= sizeof(std::uint16_t)) {
const std::uint16_t low = static_cast<std::uint16_t>(data[0]);
const std::uint16_t high = static_cast<std::uint16_t>(data[1]) << 8u;
const std::uint16_t value = static_cast<std::uint16_t>(low | high);
data += sizeof(std::uint16_t);
remaining -= sizeof(std::uint16_t);
if (value == kPsf1Separator) {
break;
}
if (value == kPsf1StartSeq) {
inSequence = true;
continue;
}
if (!inSequence) {
parsed.unicodeEntries.push_back({value, glyph});
parsed.unicodeToGlyph.emplace(value, glyph);
}
}
}
} else if (parsed.view.format == PsfFormat::Psf2) {
auto begin = parsed.view.unicode.begin();
auto end = parsed.view.unicode.end();
for (std::uint32_t glyph = 0; glyph < parsed.view.glyphCount && begin != end; ++glyph) {
bool inSequence = false;
while (begin != end) {
const std::uint8_t byte = *begin;
if (byte == kPsf2Separator) {
++begin;
break;
}
if (byte == kPsf2StartSeq) {
inSequence = true;
++begin;
continue;
}
auto it = begin;
const std::uint32_t codepoint = decodeUtf8(it, end);
if (codepoint == 0xFFFFFFFFu) {
begin = end;
break;
}
begin = it;
if (!inSequence) {
parsed.unicodeEntries.push_back({codepoint, glyph});
parsed.unicodeToGlyph.emplace(codepoint, glyph);
}
}
}
}
}
if (parsed.unicodeToGlyph.empty()) {
for (std::uint32_t glyph = 0; glyph < parsed.view.glyphCount; ++glyph) {
parsed.unicodeToGlyph.emplace(glyph, glyph);
}
}
// Choose '?' as fallback if present.
const auto it = parsed.unicodeToGlyph.find(static_cast<std::uint32_t>('?'));
if (it != parsed.unicodeToGlyph.end())
parsed.defaultGlyphIndex = it->second;
else
parsed.defaultGlyphIndex = 0;
return parsed;
}
std::uint32_t glyphIndexForCodepoint(const ParsedPsfFont& font, std::uint32_t codepoint) {
const auto it = font.unicodeToGlyph.find(codepoint);
if (it != font.unicodeToGlyph.end())
return it->second;
return font.defaultGlyphIndex;
}
} // namespace cardboy::gfx