From e36facce4704deba2e21e60f871603193f3fb1e1 Mon Sep 17 00:00:00 2001 From: Alexander Olofsson Date: Sun, 13 Apr 2025 10:38:33 +0200 Subject: [PATCH] Initial commit --- .gitignore | 3 + CMakeLists.txt | 56 ++++++++++ build_pi.sh | 55 +++++++++ src/Application.cpp | 205 ++++++++++++++++++++++++++++++++++ src/Application.hpp | 43 +++++++ src/CMakeLists.txt | 4 + src/Util/RaylibHelpers.hpp | 16 +++ src/Util/Text2D.hpp | 89 +++++++++++++++ src/Util/Text3D.cpp | 161 ++++++++++++++++++++++++++ src/Util/Text3D.hpp | 14 +++ src/Widget.hpp | 54 +++++++++ src/main.cpp | 49 ++++++++ src/resources/LysatorLogo.vox | 0 src/widgets/ClockWidget.cpp | 21 ++++ src/widgets/ClockWidget.hpp | 20 ++++ src/widgets/DummyWidget.hpp | 18 +++ src/widgets/LogoWidget.cpp | 111 ++++++++++++++++++ src/widgets/LogoWidget.hpp | 34 ++++++ src/widgets/MatrixWidget.cpp | 100 +++++++++++++++++ src/widgets/MatrixWidget.hpp | 37 ++++++ 20 files changed, 1090 insertions(+) create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100755 build_pi.sh create mode 100644 src/Application.cpp create mode 100644 src/Application.hpp create mode 100644 src/CMakeLists.txt create mode 100644 src/Util/RaylibHelpers.hpp create mode 100644 src/Util/Text2D.hpp create mode 100644 src/Util/Text3D.cpp create mode 100644 src/Util/Text3D.hpp create mode 100644 src/Widget.hpp create mode 100644 src/main.cpp create mode 100644 src/resources/LysatorLogo.vox create mode 100644 src/widgets/ClockWidget.cpp create mode 100644 src/widgets/ClockWidget.hpp create mode 100644 src/widgets/DummyWidget.hpp create mode 100644 src/widgets/LogoWidget.cpp create mode 100644 src/widgets/LogoWidget.hpp create mode 100644 src/widgets/MatrixWidget.cpp create mode 100644 src/widgets/MatrixWidget.hpp diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..170e2a3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.cache/ +build/ +build_pi/ diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..2817ec5 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,56 @@ +cmake_minimum_required(VERSION 3.24...3.30) +project(BarStatusScreen) + +include(FetchContent) + +# Generate compile_commands.json +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# Dependencies +set(RAYLIB_VERSION 5.0) + +FetchContent_Declare( + raylib + DOWNLOAD_EXTRACT_TIMESTAMP OFF + URL https://github.com/raysan5/raylib/archive/refs/tags/${RAYLIB_VERSION}.tar.gz + FIND_PACKAGE_ARGS +) + +FetchContent_MakeAvailable(raylib) + +# Our Project +add_executable(${PROJECT_NAME}) +add_subdirectory(src) + +set_target_properties(${PROJECT_NAME} PROPERTIES + RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${PROJECT_NAME}) + +set_property(TARGET ${PROJECT_NAME} PROPERTY VS_DEBUGGER_WORKING_DIRECTORY $) + +if ("${PLATFORM}" STREQUAL "Web") + add_custom_command( + TARGET ${PROJECT_NAME} PRE_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_SOURCE_DIR}/src/resources $/../resources + ) + #DEPENDS ${PROJECT_NAME} +else() + add_custom_command( + TARGET ${PROJECT_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_SOURCE_DIR}/src/resources $/resources + ) + #DEPENDS ${PROJECT_NAME} +endif() + +#set(raylib_VERBOSE 1) +target_link_libraries(${PROJECT_NAME} raylib) + +# Web Configurations +if ("${PLATFORM}" STREQUAL "Web") + # Tell Emscripten to build an example.html file. + set_target_properties(${PROJECT_NAME} PROPERTIES SUFFIX ".html") + target_link_options(${PROJECT_NAME} PUBLIC -sUSE_GLFW=3 PUBLIC --preload-file resources) +endif() diff --git a/build_pi.sh b/build_pi.sh new file mode 100755 index 0000000..430d2de --- /dev/null +++ b/build_pi.sh @@ -0,0 +1,55 @@ +#!/bin/bash + +if [ "$(uname -m)" == "aarch64" ]; then + apk add -U cmake make g++ mesa-dev + mkdir -p /build/build_pi + cd /build/build_pi + cmake .. \ + -DCMAKE_BUILD_TYPE=Release \ + -DPLATFORM=DRM \ + -DCUSTOMIZE_BUILD=ON \ + -DSUPPORT_CAMERA_SYSTEM=ON \ + -DSUPPORT_CLIPBOARD_IMAGE=OFF \ + -DSUPPORT_COMPRESSION_API=OFF \ + -DSUPPORT_DEFAULT_FONT=ON \ + -DSUPPORT_FILEFORMAT_DDS=OFF \ + -DSUPPORT_FILEFORMAT_FNT=OFF \ + -DSUPPORT_FILEFORMAT_GIF=OFF \ + -DSUPPORT_FILEFORMAT_GLTF=OFF \ + -DSUPPORT_FILEFORMAT_HDR=OFF \ + -DSUPPORT_FILEFORMAT_IQM=OFF \ + -DSUPPORT_FILEFORMAT_M3D=OFF \ + -DSUPPORT_FILEFORMAT_MOD=OFF \ + -DSUPPORT_FILEFORMAT_MP3=OFF \ + -DSUPPORT_FILEFORMAT_MTL=OFF \ + -DSUPPORT_FILEFORMAT_OBJ=OFF \ + -DSUPPORT_FILEFORMAT_OGG=OFF \ + -DSUPPORT_FILEFORMAT_PNG=ON \ + -DSUPPORT_FILEFORMAT_QOA=OFF \ + -DSUPPORT_FILEFORMAT_QOI=ON \ + -DSUPPORT_FILEFORMAT_TTF=ON \ + -DSUPPORT_FILEFORMAT_VOX=OFF \ + -DSUPPORT_FILEFORMAT_WAV=OFF \ + -DSUPPORT_FILEFORMAT_XM=OFF \ + -DSUPPORT_GESTURES_SYSTEM=OFF \ + -DSUPPORT_GIF_RECORDING=OFF \ + -DSUPPORT_IMAGE_EXPORT=OFF \ + -DSUPPORT_IMAGE_GENERATION=OFF \ + -DSUPPORT_IMAGE_MANIPULATION=OFF \ + -DSUPPORT_MESH_GENERATION=ON \ + -DSUPPORT_MODULE_RAUDIO=OFF \ + -DSUPPORT_MODULE_RMODELS=ON \ + -DSUPPORT_MODULE_RSHAPES=ON \ + -DSUPPORT_MODULE_RTEXT=ON \ + -DSUPPORT_MODULE_RTEXTURES=ON \ + -DSUPPORT_MOUSE_GESTURES=OFF \ + -DSUPPORT_QUADS_DRAW_MODE=ON \ + -DSUPPORT_SCREEN_CAPTURE=OFF \ + -DSUPPORT_SSH_KEYBOARD_RPI=OFF \ + -DSUPPORT_STANDARD_FILEIO=ON \ + -DSUPPORT_TEXT_MANIPULATION=ON \ + -DSUPPORT_WINMM_HIGHRES_TIMER=ON + make -j12 +else + podman run --rm --arch=aarch64 --pull always -v `pwd`:/build ghcr.io/linuxcontainers/alpine /bin/sh /build/build_pi.sh +fi diff --git a/src/Application.cpp b/src/Application.cpp new file mode 100644 index 0000000..3318514 --- /dev/null +++ b/src/Application.cpp @@ -0,0 +1,205 @@ +#include "Application.hpp" + +#include "Widget.hpp" +#include "widgets/DummyWidget.hpp" + +#include "raylib.h" + +#include +#include +#include +#include +#include + +using namespace BSS; + +Application::Application() + : m_shuffleInterval(30) + , m_activeWidgetCounter{} + , m_halfWidth{} + , m_halfHeight{} + , m_widgetShuffleTime{} + , m_rand(std::random_device()()) +{ + std::cout << "Starting up Bar Status Screen..." << std::endl; + + for (auto& factory : BSS::Widget::WidgetFactories) + { + auto widget = factory(); + if (!widget->IsAvailable()) + { + std::cout << " " << (widget->IsVanity() ? "(Vanity) " : "") << "Widget " << widget->GetName() << " is not available, skipping." << std::endl; + continue; + } + + std::cout << " Registering " << widget->GetName() << (widget->IsVanity() ? " vanity" : "") << " widget" << std::endl; + m_widgets.emplace_back(std::move(widget)); + } + BSS::Widget::WidgetFactories.clear(); + + for (int i = 0; i < 4; ++i) + m_activeWidgets[i] = std::make_shared(); + +#if PLATFORM_DRM + int screenWidth = 1280; + int screenHeight = 720; +#else + int screenWidth = GetMonitorWidth(0); + int screenHeight = GetMonitorHeight(0); +#endif + // SetTargetFPS(GetMonitorRefreshRate(0)); + + SetConfigFlags(FLAG_MSAA_4X_HINT | FLAG_VSYNC_HINT | FLAG_WINDOW_RESIZABLE); + InitWindow(screenWidth, screenHeight, "Bar Status Screen"); + SetWindowState(FLAG_WINDOW_MAXIMIZED | FLAG_FULLSCREEN_MODE); + SetTargetFPS(30); + + m_halfWidth = GetScreenWidth() / 2.f; + m_halfHeight = GetScreenHeight() / 2.f; + + std::cout << " Adding Logo widget." << std::endl; + AddWidget("Logo"); +} + +Application::~Application() +{ + +} + +void Application::Run() +{ + auto lastFrame = GetTime(); + while (!WindowShouldClose()) + { + const auto cur = GetTime(); + const float dt = cur - lastFrame; + lastFrame = cur; + + Update(dt); + Draw(); + } + CloseWindow(); +} + +void Application::AddWidget(const std::string& name) +{ + std::cout << "Adding named widget " << name << std::endl; + auto it = std::find_if(std::begin(m_widgets), std::end(m_widgets), [&](auto& widget) { return widget->GetName() == name; }); + if (it == std::end(m_widgets)) + std::cout << " Unable to find widged named " << name << ", ignoring." << std::endl; + else + AddWidget(*it); +} + +void Application::AddDummyWidget() +{ + AddWidget(std::make_shared()); +} + +void Application::Update(float dt) +{ + m_halfWidth = GetScreenWidth() / 2.f; + m_halfHeight = GetScreenHeight() / 2.f; + + for (auto& widget : m_widgets) + if (widget->IsActive()) + widget->Update(dt); + + m_widgetShuffleTime += dt; + if (m_widgetShuffleTime > m_shuffleInterval || IsKeyPressed(KEY_ENTER)) + { + AddRandomWidget(); + m_widgetShuffleTime = 0; + } +} + +void Application::Draw() +{ + BeginDrawing(); + + ClearBackground(BLACK); + + int i = 0; + for (auto& widget : m_activeWidgets) + { + uint8_t x = i % 2; + uint8_t y = i++ / 2; + + Rectangle rect { + x * m_halfWidth, y * m_halfHeight, + m_halfWidth, m_halfHeight + }; + + bool scissor = widget->ShouldScissor(); + if (scissor) + BeginScissorMode(rect.x, rect.y, rect.x + rect.width, rect.y + rect.height); + + widget->Draw(rect.x, rect.y, rect.width, rect.height); + + if (scissor) + EndScissorMode(); + } + + DrawFPS(0, 0); + + EndDrawing(); +} + +void Application::AddRandomWidget() +{ + std::shuffle(m_widgets.begin(), m_widgets.end(), m_rand); + + bool allowVanity = std::uniform_real_distribution()(m_rand) < 0.25; + auto it = std::end(m_widgets); + + std::cout << "Adding random widget" << (allowVanity ? " (vanity allowed)" : "") << std::endl; + + do + { + // Find first valid widget in list of loaded widgets + it = std::find_if(std::begin(m_widgets), std::end(m_widgets), [&](auto& potential) { + if (potential->IsVanity() && !allowVanity) + return false; + + // Skip if widget is already displayed + if (std::find_if(std::begin(m_activeWidgets), std::end(m_activeWidgets), [&](auto& loaded) { return loaded == potential; }) != std::end(m_activeWidgets)) + return false; + + return true; + }); + + if (it == std::end(m_widgets) && !allowVanity) + { + std::cout << " Failed to find regular widget, retrying with vanity allowed." << std::endl; + allowVanity = true; + } + else + break; + } while(true); + + if (it != std::end(m_widgets)) + AddWidget(*it); + else + std::cout << " No valid widgets to add, ignoring." << std::endl; +} + +void Application::AddWidget(const std::shared_ptr& widget) +{ + std::cout << "Adding " << widget->GetName() << " widget" << std::endl; + + if (!widget->IsActive()) + widget->Activate(m_halfWidth, m_halfHeight); + + auto active = m_activeWidgets[m_activeWidgetCounter]; + m_activeWidgets[m_activeWidgetCounter] = widget; + m_activeWidgetCounter = (m_activeWidgetCounter + 1) % m_activeWidgets.size(); + + std::cout << " Replacing " << active->GetName() << " widget" << std::endl; + + if (std::find(std::begin(m_activeWidgets), std::end(m_activeWidgets), active) == std::end(m_activeWidgets)) + { + std::cout << " Deactivating " << active->GetName() << " widget" << std::endl; + + active->Deactivate(); + } +} diff --git a/src/Application.hpp b/src/Application.hpp new file mode 100644 index 0000000..69906c9 --- /dev/null +++ b/src/Application.hpp @@ -0,0 +1,43 @@ +#pragma once + +#include "Widget.hpp" + +#include +#include +#include + +namespace BSS +{ + +class Application +{ +public: + Application(); + Application(const Application&) = delete; + ~Application(); + + void SetShuffleInterval(float interval) { m_shuffleInterval = interval; } + + void Run(); + + void AddWidget(const std::string& name); + void AddRandomWidget(); + void AddDummyWidget(); + +private: + void Update(float dt); + void Draw(); + + void AddWidget(const std::shared_ptr& widget); + + std::vector> m_widgets; + std::array, 4> m_activeWidgets; + size_t m_activeWidgetCounter; + + float m_shuffleInterval; + std::mt19937 m_rand; + + float m_halfWidth, m_halfHeight, m_widgetShuffleTime; +}; + +} diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..d1aaa38 --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,4 @@ +file(GLOB_RECURSE SOURCE_FILES CONFIGURE_DEPENDS *.cpp) +file(GLOB_RECURSE HEADER_FILES CONFIGURE_DEPENDS *.hpp) + +target_sources(${PROJECT_NAME} PRIVATE ${SOURCE_FILES} ${HEADER_FILES}) diff --git a/src/Util/RaylibHelpers.hpp b/src/Util/RaylibHelpers.hpp new file mode 100644 index 0000000..7dd839e --- /dev/null +++ b/src/Util/RaylibHelpers.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include "raylib.h" +#include "rlgl.h" + +namespace BSS::Util +{ + +class RLMatrix +{ +public: + RLMatrix() { rlPushMatrix(); } + ~RLMatrix() { rlPopMatrix(); } +}; + +} diff --git a/src/Util/Text2D.hpp b/src/Util/Text2D.hpp new file mode 100644 index 0000000..b334900 --- /dev/null +++ b/src/Util/Text2D.hpp @@ -0,0 +1,89 @@ +#pragma once + +#include "raylib.h" + +#include +#include + +namespace BSS::Util +{ + +inline void DrawTextCenter(Font font, const std::string& text, Vector2 position, float fontSize, float fontSpacing, Color textColor) +{ + if (text.find('\n') != std::string::npos) + { + const int splits = std::count(std::begin(text), std::end(text), '\n'); + const float total = splits * fontSize; + + int at = 0; + int part = 0; + do { + const int next = text.find('\n', at); + const float perc = part++ / (float)splits; + + DrawTextCenter(font, text.substr(at, next), { position.x, position.y - total * 0.5f + total * perc }, fontSize, fontSpacing, textColor); + at = next; + if (at != std::string::npos) + at++; + } while (at != std::string::npos); + return; + } + + auto size = MeasureTextEx(font, text.c_str(), fontSize, fontSpacing); + + DrawTextEx(font, text.c_str(), { position.x - size.x / 2.f, position.y - size.y / 2.f }, fontSize, fontSpacing, textColor); +} +inline void DrawTextCenterOutline(Font font, const std::string& text, Vector2 position, float fontSize, float fontSpacing, float outline, Color textColor, Color outlineColor) +{ + if (text.find('\n') != std::string::npos) + { + const int splits = std::count(std::begin(text), std::end(text), '\n'); + const float total = splits * fontSize; + + int at = 0; + int part = 0; + do { + const int next = text.find('\n', at); + const float perc = part++ / (float)splits; + + DrawTextCenterOutline(font, text.substr(at, next), { position.x, position.y - total * 0.5f + total * perc }, fontSize, fontSpacing, outline, textColor, outlineColor); + at = next; + if (at != std::string::npos) + at++; + } while (at != std::string::npos); + return; + } + + const float scale = fontSize / font.baseSize; + const char* str = text.c_str(); + const auto size = MeasureTextEx(font, str, fontSize, fontSpacing); + Vector2 pos = { position.x - size.x * 0.5f, position.y - size.y * 0.5f }; + + for (size_t i = 0; i < text.length();) + { + int codepointByteCount = 0; + const int codepoint = GetCodepoint(str + i, &codepointByteCount); + const int glyphIdx = GetGlyphIndex(font, codepoint); + + if (codepoint == 0x3f) + codepointByteCount = 1; + if (codepoint != ' ' && codepoint != '\t' && codepoint != '\n') + { + DrawTextCodepoint(font, codepoint, { pos.x - outline, pos.y - outline }, fontSize, outlineColor); + DrawTextCodepoint(font, codepoint, { pos.x - outline, pos.y + outline }, fontSize, outlineColor); + DrawTextCodepoint(font, codepoint, { pos.x + outline, pos.y - outline }, fontSize, outlineColor); + DrawTextCodepoint(font, codepoint, { pos.x + outline, pos.y + outline }, fontSize, outlineColor); + + DrawTextCodepoint(font, codepoint, pos, fontSize, textColor); + } + + if (font.glyphs[glyphIdx].advanceX == 0.f) + pos.x += (font.recs[glyphIdx].width + fontSpacing) * scale; + else + pos.x += (font.glyphs[glyphIdx].advanceX + fontSpacing) * scale; + + i += codepointByteCount; + } +} + +} diff --git a/src/Util/Text3D.cpp b/src/Util/Text3D.cpp new file mode 100644 index 0000000..c1bb960 --- /dev/null +++ b/src/Util/Text3D.cpp @@ -0,0 +1,161 @@ +#include "Text3D.hpp" + +#include "rlgl.h" + +void BSS::Util::DrawTextCodepoint3D(Font font, int codepoint, Vector3 position, float fontSize, bool backface, Color tint) +{ + // Character index position in sprite font + // NOTE: In case a codepoint is not available in the font, index returned points to '?' + int index = GetGlyphIndex(font, codepoint); + float scale = fontSize/(float)font.baseSize; + + // Character destination rectangle on screen + // NOTE: We consider charsPadding on drawing + position.x += (float)(font.glyphs[index].offsetX - font.glyphPadding)/(float)font.baseSize*scale; + position.z += (float)(font.glyphs[index].offsetY - font.glyphPadding)/(float)font.baseSize*scale; + + // Character source rectangle from font texture atlas + // NOTE: We consider chars padding when drawing, it could be required for outline/glow shader effects + Rectangle srcRec = { font.recs[index].x - (float)font.glyphPadding, font.recs[index].y - (float)font.glyphPadding, + font.recs[index].width + 2.0f*font.glyphPadding, font.recs[index].height + 2.0f*font.glyphPadding }; + + float width = (float)(font.recs[index].width + 2.0f*font.glyphPadding)/(float)font.baseSize*scale; + float height = (float)(font.recs[index].height + 2.0f*font.glyphPadding)/(float)font.baseSize*scale; + + if (font.texture.id > 0) + { + const float x = 0.0f; + const float y = 0.0f; + const float z = 0.0f; + + // normalized texture coordinates of the glyph inside the font texture (0.0f -> 1.0f) + const float tx = srcRec.x/font.texture.width; + const float ty = srcRec.y/font.texture.height; + const float tw = (srcRec.x+srcRec.width)/font.texture.width; + const float th = (srcRec.y+srcRec.height)/font.texture.height; + + rlCheckRenderBatchLimit(4 + 4*backface); + rlSetTexture(font.texture.id); + + rlPushMatrix(); + rlTranslatef(position.x, position.y, position.z); + + rlBegin(RL_QUADS); + rlColor4ub(tint.r, tint.g, tint.b, tint.a); + + // Front Face + rlNormal3f(0.0f, 1.0f, 0.0f); // Normal Pointing Up + rlTexCoord2f(tx, ty); rlVertex3f(x, y, z); // Top Left Of The Texture and Quad + rlTexCoord2f(tx, th); rlVertex3f(x, y, z + height); // Bottom Left Of The Texture and Quad + rlTexCoord2f(tw, th); rlVertex3f(x + width, y, z + height); // Bottom Right Of The Texture and Quad + rlTexCoord2f(tw, ty); rlVertex3f(x + width, y, z); // Top Right Of The Texture and Quad + + if (backface) + { + // Back Face + rlNormal3f(0.0f, -1.0f, 0.0f); // Normal Pointing Down + rlTexCoord2f(tx, ty); rlVertex3f(x, y, z); // Top Right Of The Texture and Quad + rlTexCoord2f(tw, ty); rlVertex3f(x + width, y, z); // Top Left Of The Texture and Quad + rlTexCoord2f(tw, th); rlVertex3f(x + width, y, z + height); // Bottom Left Of The Texture and Quad + rlTexCoord2f(tx, th); rlVertex3f(x, y, z + height); // Bottom Right Of The Texture and Quad + } + rlEnd(); + rlPopMatrix(); + + rlSetTexture(0); + } +} + +void BSS::Util::DrawText3D(Font font, const std::string& text, Vector3 position, float fontSize, float fontSpacing, float lineSpacing, bool backface, Color tint) +{ + float textOffsetY = 0.0f; // Offset between lines (on line break '\n') + float textOffsetX = 0.0f; // Offset X to next character to draw + + float scale = fontSize/(float)font.baseSize; + + for (int i = 0; i < text.size();) + { + // Get next codepoint from byte string and glyph index in font + int codepointByteCount = 0; + int codepoint = GetCodepoint(&text[i], &codepointByteCount); + int index = GetGlyphIndex(font, codepoint); + + // NOTE: Normally we exit the decoding sequence as soon as a bad byte is found (and return 0x3f) + // but we need to draw all of the bad bytes using the '?' symbol moving one byte + if (codepoint == 0x3f) codepointByteCount = 1; + + if (codepoint == '\n') + { + // NOTE: Fixed line spacing of 1.5 line-height + // TODO: Support custom line spacing defined by user + textOffsetY += scale + lineSpacing/(float)font.baseSize*scale; + textOffsetX = 0.0f; + } + else + { + if ((codepoint != ' ') && (codepoint != '\t')) + { + DrawTextCodepoint3D(font, codepoint, (Vector3){ position.x + textOffsetX, position.y, position.z + textOffsetY }, fontSize, backface, tint); + } + + if (font.glyphs[index].advanceX == 0) textOffsetX += (float)(font.recs[index].width + fontSpacing)/(float)font.baseSize*scale; + else textOffsetX += (float)(font.glyphs[index].advanceX + fontSpacing)/(float)font.baseSize*scale; + } + + i += codepointByteCount; // Move text bytes counter to next codepoint + } +} + +Vector3 BSS::Util::MeasureText3D(Font font, const std::string& text, Vector3 position, float fontSize, float fontSpacing, float lineSpacing, bool backface, Color tint) +{ + int tempLen = 0; // Used to count longer text line num chars + int lenCounter = 0; + + float tempTextWidth = 0.0f; // Used to count longer text line width + + float scale = fontSize/(float)font.baseSize; + float textHeight = scale; + float textWidth = 0.0f; + + int letter = 0; // Current character + int index = 0; // Index position in sprite font + + for (int i = 0; i < text.size(); i++) + { + lenCounter++; + + int next = 0; + letter = GetCodepoint(&text[i], &next); + index = GetGlyphIndex(font, letter); + + // NOTE: normally we exit the decoding sequence as soon as a bad byte is found (and return 0x3f) + // but we need to draw all of the bad bytes using the '?' symbol so to not skip any we set next = 1 + if (letter == 0x3f) + next = 1; + i += next - 1; + + if (letter != '\n') + { + if (font.glyphs[index].advanceX != 0) textWidth += (font.glyphs[index].advanceX+fontSpacing)/(float)font.baseSize*scale; + else textWidth += (font.recs[index].width + font.glyphs[index].offsetX)/(float)font.baseSize*scale; + } + else + { + if (tempTextWidth < textWidth) tempTextWidth = textWidth; + lenCounter = 0; + textWidth = 0.0f; + textHeight += scale + lineSpacing/(float)font.baseSize*scale; + } + + if (tempLen < lenCounter) tempLen = lenCounter; + } + + if (tempTextWidth < textWidth) tempTextWidth = textWidth; + + Vector3 vec = { 0 }; + vec.x = tempTextWidth + (float)((tempLen - 1)*fontSpacing/(float)font.baseSize*scale); // Adds chars spacing to measure + vec.y = 0.25f; + vec.z = textHeight; + + return vec; +} diff --git a/src/Util/Text3D.hpp b/src/Util/Text3D.hpp new file mode 100644 index 0000000..f60c763 --- /dev/null +++ b/src/Util/Text3D.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include "raylib.h" + +#include + +namespace BSS::Util +{ + +extern void DrawTextCodepoint3D(Font font, int codepoint, Vector3 position, float fontSize, bool backface, Color tint); +extern void DrawText3D(Font font, const std::string& text, Vector3 position, float fontSize, float fontSpacing, float lineSpacing, bool backface, Color tint); +extern Vector3 MeasureText3D(Font font, const std::string& text, Vector3 position, float fontSize, float fontSpacing, float lineSpacing, bool backface, Color tint); + +} diff --git a/src/Widget.hpp b/src/Widget.hpp new file mode 100644 index 0000000..b80ea12 --- /dev/null +++ b/src/Widget.hpp @@ -0,0 +1,54 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include + +namespace BSS +{ + +class Widget +{ +public: + Widget() : m_activated(false) {} + virtual ~Widget() { if (IsActive()) Deactivate(); } + + static std::list()>> WidgetFactories; + + virtual std::string GetName() const = 0; + + virtual void Init() {} + virtual void Deinit() {} + virtual bool IsAvailable() { return true; } + + virtual void Activate(float w, float h) { m_activated = true; LogLine("Activated"); } + virtual void Deactivate() { m_activated = false; } + virtual bool IsActive() { return m_activated; } + + virtual bool ShouldScissor() const { return true; } + virtual bool IsVanity() const { return false; } + + virtual void Update(float dt) = 0; + virtual void Draw(float x, float y, float width, float height) = 0; + +protected: + void LogLine(const char* format, ...) { + printf("[%s] ", GetName().c_str()); + va_list args; + va_start(args, format); + vprintf(format, args); + va_end(args); + printf("\n"); + } + +private: + bool m_activated; +}; + +#define IMPLEMENT_WIDGET(WIDGET) namespace { int dummy = (::BSS::Widget::WidgetFactories.push_back([]() { return std::make_shared(); }), 0); }; \ + +} diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..2a923b9 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,49 @@ +#include "Application.hpp" +#include "Widget.hpp" + +#include +#include +#include + +#include +#include + +std::list()>> BSS::Widget::WidgetFactories{}; +std::unique_ptr app; + +void sighup_handler(int sig) +{ + static const std::filesystem::path widgetPath = "/tmp/bss-widget"; + std::cout << "Received sighup, adding widget." << std::endl; + + if (std::filesystem::exists(widgetPath)) + { + auto file = std::ifstream(widgetPath); + std::string widgetName; + file >> widgetName; + std::filesystem::remove(widgetPath); + + app->AddWidget(widgetName); + } + else + app->AddRandomWidget(); +} + +int main(void) +{ + app = std::make_unique(); + + std::signal(SIGHUP, sighup_handler); + + //for (int i = 0; i < 3; ++i) + // app->AddRandomWidget(); + + //app.AddWidget("Clock"); + //app.AddWidget("Logo"); + //app.AddWidget("Matrix"); + + //app.SetShuffleInterval(5); + app->Run(); + + return 0; +} diff --git a/src/resources/LysatorLogo.vox b/src/resources/LysatorLogo.vox new file mode 100644 index 0000000..e69de29 diff --git a/src/widgets/ClockWidget.cpp b/src/widgets/ClockWidget.cpp new file mode 100644 index 0000000..ec80f98 --- /dev/null +++ b/src/widgets/ClockWidget.cpp @@ -0,0 +1,21 @@ +#include "ClockWidget.hpp" + +#include "../Util/Text2D.hpp" + +#include "raylib.h" + +#include + +using namespace BSS::Widgets; + +IMPLEMENT_WIDGET(ClockWidget); + +void ClockWidget::Draw(float x, float y, float width, float height) +{ + static char timeString[std::size("yyyy-mm-dd hh:mm:ss")]; + + const std::time_t time = std::time({}); + std::strftime(std::data(timeString), std::size(timeString), "%F\n%T", std::gmtime(&time)); + + Util::DrawTextCenterOutline(GetFontDefault(), timeString, { x + width * 0.5f, y + height * 0.5f }, height / 8.f, 1.f, height / 200.f, BLACK, GRAY); +} diff --git a/src/widgets/ClockWidget.hpp b/src/widgets/ClockWidget.hpp new file mode 100644 index 0000000..fdc49b1 --- /dev/null +++ b/src/widgets/ClockWidget.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include "../Widget.hpp" + +namespace BSS::Widgets +{ + +// A widget displaying the time +class ClockWidget : public Widget +{ +public: + std::string GetName() const override { return "Clock"; } + + void Update(float dt) override {} + void Draw(float x, float y, float width, float height) override; + + bool IsVanity() const override { return true; } +}; + +} diff --git a/src/widgets/DummyWidget.hpp b/src/widgets/DummyWidget.hpp new file mode 100644 index 0000000..a2d8ce2 --- /dev/null +++ b/src/widgets/DummyWidget.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include "../Widget.hpp" + +namespace BSS::Widgets +{ + +// A "This space left intentionally blank" widget +class DummyWidget : public Widget +{ +public: + inline std::string GetName() const override { return "Dummy"; } + + inline void Update(float dt) override {} + inline void Draw(float x, float y, float width, float height) override {} +}; + +} diff --git a/src/widgets/LogoWidget.cpp b/src/widgets/LogoWidget.cpp new file mode 100644 index 0000000..dcfdc8e --- /dev/null +++ b/src/widgets/LogoWidget.cpp @@ -0,0 +1,111 @@ +#include "LogoWidget.hpp" + +#include "../Util/Text2D.hpp" + +#include "raylib.h" +#include "rlgl.h" + +#include + +using namespace BSS::Widgets; + +IMPLEMENT_WIDGET(LogoWidget); + +// Morris Maroon / PDP11 +const Color Color_CtrlC_Blue{ 99, 15, 30, 255 }; +// Brite Copenhagen Blue / PDP15 +const Color Color_CtrlC_Red{ 39, 128, 196, 255 }; +// Pure yellow +const Color Color_Lysator{ 255, 255, 0, 255 }; + +LogoWidget::LogoWidget() +{ +} + +void LogoWidget::Activate(float w, float h) +{ + Widget::Activate(w, h); + + m_font = GetFontDefault(); + + PreRender(w, h); +} + +void LogoWidget::Deactivate() +{ + Widget::Deactivate(); + + UnloadFont(m_font); + UnloadRenderTexture(m_texture); + m_texture.texture.width = -1; +} + +void LogoWidget::Draw(float x, float y, float w, float h) +{ + if (m_texture.texture.width != (int)w || m_texture.texture.height != (int)h) + { + EndScissorMode(); + PreRender(w, h); + BeginScissorMode(x, y, w, h); + } + + // Lysator + DrawTextureRec(m_texture.texture, { (float)m_texture.texture.width * 0.25f, 0, (float)m_texture.texture.width, (float)-m_texture.texture.height }, { x, y - h * 0.05f }, WHITE); + + // Ctrl-C + { + const auto mid = Vector2{ x + w * 0.75f, y + h * 0.45f }; + const float base = h / 5.f; + + Util::DrawTextCenterOutline(m_font, "C", mid, base * 2, 1.f, h / 250.f, Color_CtrlC_Red, WHITE); + Util::DrawTextCenterOutline(m_font, "Ctrl", { mid.x - base * 0.25f, mid.y }, base * 0.75f, 1.f, h / 250.f, Color_CtrlC_Blue, WHITE); + } + + Util::DrawTextCenterOutline(m_font, "Bar\nMonitoring", { x + w * 0.5f, y + h * 0.5f }, cos(GetTime() * 0.5f) * 5.f + h / 4.f, 1.f, h / 200.f, BLACK, GRAY); +} + +void LogoWidget::PreRender(float w, float h) +{ + if (m_texture.texture.width == (int)w && m_texture.texture.height == (int)h) + return; + + LogLine("Rendering %fx%fpx texture of Lysator logo", w, h); + + auto torusMesh = GenMeshTorus(0.15f, 0.5f, 16, 16); + auto torusModel = LoadModelFromMesh(torusMesh); + + if (IsRenderTextureReady(m_texture)) + UnloadRenderTexture(m_texture); + + m_texture = LoadRenderTexture(w, h); + + BeginTextureMode(m_texture); + ClearBackground(BLACK); + + Camera camera = { 0 }; + camera.position = (Vector3){ 0.0f, 0.0f, 12.0f }; // Camera position + camera.target = (Vector3){ 0.0f, 0.0f, 0.0f }; // Camera looking at point + camera.up = (Vector3){ 0.0f, 1.0f, 0.0f }; // Camera up vector (rotation towards target) + camera.fovy = 45.0f; // Camera field-of-view Y + camera.projection = CAMERA_PERSPECTIVE; // Camera projection type + BeginMode3D(camera); + + // Lysator + { + // X + DrawCube({ 0, 0, 0 }, 8.f, 0.3f, 0.3f, Color_Lysator); + // Y + DrawCube({ 0, 0, 0 }, 0.3f, 8.f, 0.3f, Color_Lysator); + // Z + DrawCube({ 0, 0, 0 }, 0.3f, 0.3f, 8.f, Color_Lysator); + + DrawModelEx( torusModel, { 0, 0, 0 }, { 0, -0.75f, 1.f }, -90.f, { 8.f, 8.f, 8.f }, Color_Lysator); + //DrawModelWiresEx(torusModel, { 0, 0, 0 }, { 0, -0.75f, 1.f }, -90.f, { 8.f, 8.f, 8.f }, BLACK); + } + + EndMode3D(); + + EndTextureMode(); + + UnloadModel(torusModel); +} diff --git a/src/widgets/LogoWidget.hpp b/src/widgets/LogoWidget.hpp new file mode 100644 index 0000000..9f67dc3 --- /dev/null +++ b/src/widgets/LogoWidget.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include "../Widget.hpp" + +#include "raylib.h" + +namespace BSS::Widgets +{ + +// A widget displaying the status screen logo +class LogoWidget : public Widget +{ +public: + LogoWidget(); + + std::string GetName() const override { return "Logo"; } + + void Activate(float w, float h) override; + void Deactivate() override; + + void Update(float dt) override {} + void Draw(float x, float y, float width, float height) override; + + bool IsVanity() const override { return true; } + // bool ShouldScissor() const override { return false; } + +private: + void PreRender(float w, float h); + + RenderTexture2D m_texture; + Font m_font; +}; + +} diff --git a/src/widgets/MatrixWidget.cpp b/src/widgets/MatrixWidget.cpp new file mode 100644 index 0000000..3011785 --- /dev/null +++ b/src/widgets/MatrixWidget.cpp @@ -0,0 +1,100 @@ +#include "MatrixWidget.hpp" + +#include "../Util/Text2D.hpp" + +#include "raylib.h" + +#include +#include +#include +#include + +using namespace BSS::Widgets; + +IMPLEMENT_WIDGET(MatrixWidget); + +void MatrixWidget::Activate(float w, float h) +{ + Widget::Activate(w, h); + + m_rand = std::mt19937(std::random_device()()); + + float x = 0; + for (size_t i = 0; i < m_strands.size(); ++i) + { + using namespace std::numbers; + x = std::fmod(x + (float)phi, 0.95f) + 0.025f, + + m_strands[i] = { + (int)m_rand(), + x, + 0, + std::uniform_real_distribution(0, 0.5f)(m_rand), + std::uniform_int_distribution(1, 25)(m_rand), + std::uniform_int_distribution(3, 15)(m_rand) + }; + } +} + +void MatrixWidget::Update(float dt) +{ + const float step = 0.1f; + m_accum += dt; + + while (m_accum > step) + { + m_accum -= step; + + for (auto& strand : m_strands) + { + strand.accum += step; + if (strand.accum >= strand.speed) + { + strand.accum = 0; + strand.length++; + } + + if (strand.length > 25) + { + using namespace std::numbers; + strand.seed = m_rand(); + strand.x = std::fmod(strand.x + (float)phi, 0.95f) + 0.025f; + strand.speed = std::uniform_real_distribution(0, 0.5f)(m_rand); + strand.length = 1; + strand.renderLen = std::uniform_int_distribution(3, 15)(m_rand); + } + } + } +} + +void MatrixWidget::Draw(float x, float y, float width, float height) +{ + const static std::string chars = "ABCDEFGHIJLKMNOPQRSTUVWXYZ0123456789"; + const static Font font = GetFontDefault(); + + std::uniform_int_distribution charDist(0, std::size(chars) - 1); + for (const auto& strand : m_strands) + { + std::mt19937 twister(strand.seed); + + for (size_t i = 0; i < strand.length; ++i) + { + const auto chr = chars[charDist(twister)]; + const size_t dist = strand.length - i; + const auto maxDist = strand.renderLen; + + if (dist > maxDist) + continue; + + const float visibility = ((maxDist + 1) - dist) / (float)(maxDist + 1); + + auto mainCol = DARKGREEN; + auto outCol = GREEN; + + mainCol.a = 255 * visibility; + outCol.a = 255 * visibility; + + Util::DrawTextCenterOutline(font, std::string(&chr, 1), { x + width * strand.x, y + (height / 10.f) * i }, height / 20.f, 1.f, height / 500.f, mainCol, outCol); + } + } +} diff --git a/src/widgets/MatrixWidget.hpp b/src/widgets/MatrixWidget.hpp new file mode 100644 index 0000000..e83d5f8 --- /dev/null +++ b/src/widgets/MatrixWidget.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include "../Widget.hpp" + +#include +#include + +namespace BSS::Widgets +{ + +// A widget displaying Matrix-style falling letters +class MatrixWidget : public Widget +{ +public: + std::string GetName() const override { return "Matrix"; } + + void Activate(float w, float h) override; + + void Update(float dt) override; + void Draw(float x, float y, float width, float height) override; + + bool IsVanity() const override { return true; } + +private: + struct TextStrand + { + int seed; + float x, accum, speed; + size_t length, renderLen; + }; + + float m_accum; + std::mt19937 m_rand; + std::array m_strands; +}; + +}