move into firmware subfolder
Signed-off-by: Peter Siegmund <developer@mars3142.org>
This commit is contained in:
26
firmware/src/Common.cpp
Normal file
26
firmware/src/Common.cpp
Normal file
@@ -0,0 +1,26 @@
|
||||
#include "Common.h"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
auto CreateWindow(const char *title, const int width, const int height) -> Window *
|
||||
{
|
||||
|
||||
constexpr uint32_t window_flag = SDL_WINDOW_HIDDEN;
|
||||
const auto window = SDL_CreateWindow(title, width, height, window_flag);
|
||||
if (window == nullptr)
|
||||
{
|
||||
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Couldn't create window", SDL_GetError(), nullptr);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const SDL_PropertiesID props = SDL_CreateProperties();
|
||||
SDL_SetPointerProperty(props, SDL_PROP_RENDERER_CREATE_WINDOW_POINTER, window);
|
||||
SDL_SetNumberProperty(props, SDL_PROP_RENDERER_CREATE_OUTPUT_COLORSPACE_NUMBER, SDL_COLORSPACE_SRGB_LINEAR);
|
||||
if (const auto renderer = SDL_CreateRendererWithProperties(props); renderer == nullptr)
|
||||
{
|
||||
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Couldn't create renderer", SDL_GetError(), nullptr);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return new Window(window);
|
||||
}
|
60
firmware/src/Common.h
Normal file
60
firmware/src/Common.h
Normal file
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* @file Common.h
|
||||
* @brief Common utility functions and window management for application framework
|
||||
* @details This header defines common utility functions that are shared across
|
||||
* the application framework. It provides essential functionality for
|
||||
* window creation and management, serving as a bridge between the
|
||||
* application layer and the underlying windowing system.
|
||||
* @author System Control Team
|
||||
* @date 2025-06-20
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "model/Window.h"
|
||||
|
||||
/**
|
||||
* @brief Creates a new window with specified title and dimensions
|
||||
* @param title Null-terminated string containing the window title text
|
||||
* @param width Window width in pixels
|
||||
* @param height Window height in pixels
|
||||
* @return Pointer to the newly created Window object, or nullptr on failure
|
||||
*
|
||||
* @pre title must not be nullptr and should contain valid display text
|
||||
* @pre width and height must be positive values within system display limits
|
||||
* @post A new Window object is allocated and initialized with the specified parameters
|
||||
* @post The returned Window pointer is ready for use with window management functions
|
||||
*
|
||||
* @details This function creates a new Window instance with the specified
|
||||
* title and dimensions. It handles the underlying window system
|
||||
* initialization, memory allocation, and setup required to create
|
||||
* a functional window object.
|
||||
*
|
||||
* The window creation process includes:
|
||||
* - Memory allocation for the Window structure
|
||||
* - Initialization of window properties (title, dimensions, state)
|
||||
* - Registration with the window management system
|
||||
* - Setup of default window behavior and event handling
|
||||
*
|
||||
* The returned window pointer can be used with other window management
|
||||
* functions to display content, handle events, and manage the window
|
||||
* lifecycle. The caller is responsible for properly managing the window
|
||||
* lifetime and ensuring proper cleanup when the window is no longer needed.
|
||||
*
|
||||
* @note The returned pointer must be properly managed by the caller
|
||||
* @note Window resources should be freed when no longer needed
|
||||
* @note The title string is copied internally and can be safely modified
|
||||
* or freed after this function returns
|
||||
*
|
||||
* @see Window class for window object interface and methods
|
||||
*
|
||||
* Example usage:
|
||||
* @code
|
||||
* auto* window = CreateWindow("System Control", 800, 600);
|
||||
* if (window != nullptr) {
|
||||
* // Use the window...
|
||||
* // Clean up when done
|
||||
* }
|
||||
* @endcode
|
||||
*/
|
||||
auto CreateWindow(const char *title, int width, int height) -> Window *;
|
10
firmware/src/Version.h
Normal file
10
firmware/src/Version.h
Normal file
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include "string"
|
||||
|
||||
const std::string MyProject = "system_control";
|
||||
const std::string MyProjectVersion = "0.0.1";
|
||||
constexpr uint8_t MyProjectVersionMajor = 0;
|
||||
constexpr uint8_t MyProjectVersionMinor = 0;
|
||||
constexpr uint8_t MyProjectVersionPatch = 1;
|
||||
const std::string MyProjectBuildDate = "";
|
10
firmware/src/Version.h.in
Normal file
10
firmware/src/Version.h.in
Normal file
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include "string"
|
||||
|
||||
const std::string MyProject = "@PROJECT_NAME@";
|
||||
const std::string MyProjectVersion = "@PROJECT_VERSION@";
|
||||
constexpr uint8_t MyProjectVersionMajor = @PROJECT_VERSION_MAJOR@;
|
||||
constexpr uint8_t MyProjectVersionMinor = @PROJECT_VERSION_MINOR@;
|
||||
constexpr uint8_t MyProjectVersionPatch = @PROJECT_VERSION_PATCH@;
|
||||
const std::string MyProjectBuildDate = "@PROJECT_BUILD_DATE@";
|
110
firmware/src/debug/debug_overlay.cpp
Normal file
110
firmware/src/debug/debug_overlay.cpp
Normal file
@@ -0,0 +1,110 @@
|
||||
#include "debug/debug_overlay.h"
|
||||
|
||||
#include "Common.h"
|
||||
#include "Matrix.h"
|
||||
#include "Version.h"
|
||||
#include "imgui.h"
|
||||
#include "imgui_impl_sdl3.h"
|
||||
#include <imgui_impl_sdlrenderer3.h>
|
||||
|
||||
namespace DebugOverlay
|
||||
{
|
||||
void Init(const AppContext *context)
|
||||
{
|
||||
IMGUI_CHECKVERSION();
|
||||
ImGui::CreateContext();
|
||||
ImGuiIO &io{ImGui::GetIO()};
|
||||
|
||||
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
|
||||
|
||||
ImGui::StyleColorsDark();
|
||||
|
||||
ImGui_ImplSDL3_InitForSDLRenderer(context->MainWindow(), context->MainRenderer());
|
||||
ImGui_ImplSDLRenderer3_Init(context->MainRenderer());
|
||||
}
|
||||
|
||||
void Update(AppContext *context, const SDL_Event *event)
|
||||
{
|
||||
ImGui_ImplSDL3_ProcessEvent(event);
|
||||
|
||||
if (show_led_matrix)
|
||||
{
|
||||
if (!context->LedMatrixRenderer())
|
||||
{
|
||||
const auto win = CreateWindow("LED Matrix", width * 50, height * 50);
|
||||
SDL_SetWindowFocusable(win->window(), false);
|
||||
SDL_SetRenderVSync(win->renderer(), SDL_RENDERER_VSYNC_ADAPTIVE);
|
||||
SDL_SetWindowPosition(win->window(), 0, 0);
|
||||
SDL_ShowWindow(win->window());
|
||||
|
||||
const auto windowId = SDL_GetWindowID(win->window());
|
||||
context->SetMatrix(new Matrix(windowId, win->renderer(), width, height));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (context->LedMatrixRenderer())
|
||||
{
|
||||
int window_count = 0;
|
||||
if (SDL_Window **windows = SDL_GetWindows(&window_count))
|
||||
{
|
||||
for (int i = 0; i < window_count; ++i)
|
||||
{
|
||||
if (SDL_Window *window = windows[i]; context->LedMatrixId() == SDL_GetWindowID(window))
|
||||
{
|
||||
SDL_DestroyRenderer(context->LedMatrixRenderer());
|
||||
SDL_DestroyWindow(window);
|
||||
break;
|
||||
}
|
||||
}
|
||||
SDL_free(windows);
|
||||
}
|
||||
|
||||
SDL_DestroyRenderer(context->LedMatrixRenderer());
|
||||
|
||||
context->SetMatrix(nullptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Render(const AppContext *context)
|
||||
{
|
||||
ImGui_ImplSDLRenderer3_NewFrame();
|
||||
ImGui_ImplSDL3_NewFrame();
|
||||
ImGui::NewFrame();
|
||||
|
||||
if (show_debug_window && ImGui::BeginMainMenuBar())
|
||||
{
|
||||
if (ImGui::BeginMenu("Config"))
|
||||
{
|
||||
ImGui::Checkbox("Show LED Matrix", &show_led_matrix);
|
||||
ImGui::Checkbox("Show Unhandled Events", &show_unhandled_events);
|
||||
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
if (ImGui::BeginMenu("Help"))
|
||||
{
|
||||
ImGui::Text("FPS: %.2f", ImGui::GetIO().Framerate);
|
||||
ImGui::SeparatorText("App Info");
|
||||
ImGui::Text("Project: %s", MyProject.c_str());
|
||||
ImGui::Text("Version: %s", MyProjectVersion.c_str());
|
||||
ImGui::Text("Build Date: %s", MyProjectBuildDate.c_str());
|
||||
ImGui::Text("ImGui Version: %s", ImGui::GetVersion());
|
||||
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
ImGui::EndMainMenuBar();
|
||||
}
|
||||
|
||||
// Rendering
|
||||
ImGui::Render();
|
||||
ImGui_ImplSDLRenderer3_RenderDrawData(ImGui::GetDrawData(), context->MainRenderer());
|
||||
}
|
||||
|
||||
void Cleanup()
|
||||
{
|
||||
ImGui_ImplSDLRenderer3_Shutdown();
|
||||
ImGui_ImplSDL3_Shutdown();
|
||||
ImGui::DestroyContext();
|
||||
}
|
||||
} // namespace DebugOverlay
|
23
firmware/src/debug/debug_overlay.h
Normal file
23
firmware/src/debug/debug_overlay.h
Normal file
@@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include "model/AppContext.h"
|
||||
|
||||
namespace DebugOverlay
|
||||
{
|
||||
inline bool show_debug_window = false;
|
||||
inline bool show_unhandled_events = false;
|
||||
inline bool show_led_matrix = false;
|
||||
|
||||
constexpr auto width = 8;
|
||||
constexpr auto height = 8;
|
||||
|
||||
void Init(const AppContext *context);
|
||||
|
||||
void Update(AppContext *context, const SDL_Event *event);
|
||||
|
||||
void Render(const AppContext *context);
|
||||
|
||||
void Cleanup();
|
||||
} // namespace DebugOverlay
|
11
firmware/src/hal/u8g2_hal_sdl.h
Normal file
11
firmware/src/hal/u8g2_hal_sdl.h
Normal file
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include "u8g2.h"
|
||||
|
||||
#define U8G2_SCREEN_WIDTH (128)
|
||||
#define U8G2_SCREEN_HEIGHT (64)
|
||||
#define U8G2_SCREEN_FACTOR (3)
|
||||
#define U8G2_SCREEN_PADDING (25)
|
||||
|
||||
uint8_t u8x8_byte_sdl_hw_spi(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr);
|
||||
uint8_t u8x8_gpio_and_delay_sdl(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr);
|
50
firmware/src/hal/u8x8_hal_sdl.cpp
Normal file
50
firmware/src/hal/u8x8_hal_sdl.cpp
Normal file
@@ -0,0 +1,50 @@
|
||||
#include "hal/u8g2_hal_sdl.h"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <cstdint>
|
||||
|
||||
uint8_t u8x8_byte_sdl_hw_spi(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr)
|
||||
{
|
||||
switch (msg)
|
||||
{
|
||||
case U8X8_MSG_BYTE_SEND:
|
||||
case U8X8_MSG_BYTE_INIT:
|
||||
case U8X8_MSG_BYTE_SET_DC:
|
||||
case U8X8_MSG_BYTE_START_TRANSFER:
|
||||
case U8X8_MSG_BYTE_END_TRANSFER:
|
||||
break;
|
||||
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
uint8_t u8x8_gpio_and_delay_sdl(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr)
|
||||
{
|
||||
switch (msg)
|
||||
{
|
||||
case U8X8_MSG_DELAY_MILLI:
|
||||
SDL_Delay(arg_int);
|
||||
break;
|
||||
|
||||
case U8X8_MSG_DELAY_10MICRO:
|
||||
SDL_Delay((arg_int + 99) / 100);
|
||||
break;
|
||||
|
||||
case U8X8_MSG_DELAY_100NANO:
|
||||
case U8X8_MSG_DELAY_NANO:
|
||||
SDL_Delay(1);
|
||||
break;
|
||||
|
||||
case U8X8_MSG_GPIO_AND_DELAY_INIT:
|
||||
case U8X8_MSG_GPIO_RESET:
|
||||
case U8X8_MSG_GPIO_CS:
|
||||
case U8X8_MSG_GPIO_DC:
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return 1;
|
||||
}
|
189
firmware/src/main.cpp
Normal file
189
firmware/src/main.cpp
Normal file
@@ -0,0 +1,189 @@
|
||||
#include <SDL3/SDL.h>
|
||||
#include <SDL3/SDL_main.h>
|
||||
#include <vector>
|
||||
|
||||
#include <u8g2.h>
|
||||
|
||||
#include "Common.h"
|
||||
#include "common/Common.h"
|
||||
#include "debug/debug_overlay.h"
|
||||
#include "hal/u8g2_hal_sdl.h"
|
||||
#include "model/AppContext.h"
|
||||
#include "ui/Device.h"
|
||||
#include "ui/UIWidget.h"
|
||||
#include "ui/widgets/Button.h"
|
||||
#include "ui/widgets/D_Pad.h"
|
||||
|
||||
constexpr unsigned int WINDOW_WIDTH = (U8G2_SCREEN_WIDTH * U8G2_SCREEN_FACTOR + 3 * U8G2_SCREEN_PADDING +
|
||||
2 * BUTTON_WIDTH + DPAD_WIDTH + 2 * U8G2_SCREEN_PADDING);
|
||||
constexpr unsigned int WINDOW_HEIGHT = (U8G2_SCREEN_HEIGHT * U8G2_SCREEN_FACTOR + 50);
|
||||
|
||||
std::shared_ptr<Device> device;
|
||||
std::vector<std::shared_ptr<UIWidget>> widgets;
|
||||
|
||||
static uint64_t lastFrameTime = 0;
|
||||
|
||||
SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[])
|
||||
{
|
||||
if (SDL_Init(SDL_INIT_VIDEO) == false)
|
||||
{
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Couldn't initialize SDL! -> %s", SDL_GetError());
|
||||
return SDL_APP_FAILURE;
|
||||
}
|
||||
|
||||
if (TTF_Init() == false)
|
||||
{
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Couldn't initialize TTF! -> %s", SDL_GetError());
|
||||
return SDL_APP_FAILURE;
|
||||
}
|
||||
|
||||
const auto win = CreateWindow("System Control (Simulator)", WINDOW_WIDTH, WINDOW_HEIGHT);
|
||||
if (!win)
|
||||
{
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Couldn't create window! -> %s", SDL_GetError());
|
||||
return SDL_APP_FAILURE;
|
||||
}
|
||||
SDL_SetRenderVSync(win->renderer(), SDL_RENDERER_VSYNC_ADAPTIVE);
|
||||
SDL_SetWindowPosition(win->window(), SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
|
||||
SDL_ShowWindow(win->window());
|
||||
|
||||
const auto context = new AppContext(win);
|
||||
*appstate = context;
|
||||
|
||||
device = std::make_shared<Device>(context);
|
||||
widgets.push_back(device);
|
||||
|
||||
DebugOverlay::Init(context);
|
||||
|
||||
return SDL_APP_CONTINUE;
|
||||
}
|
||||
|
||||
SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event)
|
||||
{
|
||||
const auto context = static_cast<AppContext *>(appstate);
|
||||
DebugOverlay::Update(context, event);
|
||||
|
||||
switch (event->type)
|
||||
{
|
||||
case SDL_EVENT_WINDOW_CLOSE_REQUESTED:
|
||||
/// multi window
|
||||
if (SDL_GetWindowID(context->MainWindow()) == event->window.windowID)
|
||||
{
|
||||
return SDL_APP_SUCCESS;
|
||||
}
|
||||
break;
|
||||
|
||||
case SDL_EVENT_QUIT:
|
||||
/// single window application
|
||||
return SDL_APP_SUCCESS;
|
||||
|
||||
case SDL_EVENT_KEY_DOWN:
|
||||
switch (event->key.key)
|
||||
{
|
||||
case SDLK_ESCAPE:
|
||||
return SDL_APP_SUCCESS;
|
||||
|
||||
case SDLK_UP:
|
||||
device->OnButtonClicked(ButtonType::UP);
|
||||
break;
|
||||
|
||||
case SDLK_DOWN:
|
||||
device->OnButtonClicked(ButtonType::DOWN);
|
||||
break;
|
||||
|
||||
case SDLK_LEFT:
|
||||
device->OnButtonClicked(ButtonType::LEFT);
|
||||
break;
|
||||
|
||||
case SDLK_RIGHT:
|
||||
device->OnButtonClicked(ButtonType::RIGHT);
|
||||
break;
|
||||
|
||||
case SDLK_RETURN:
|
||||
device->OnButtonClicked(ButtonType::SELECT);
|
||||
break;
|
||||
|
||||
case SDLK_BACKSPACE:
|
||||
device->OnButtonClicked(ButtonType::BACK);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case SDL_EVENT_KEY_UP:
|
||||
if (event->key.key == SDLK_LSHIFT)
|
||||
{
|
||||
DebugOverlay::show_debug_window = !DebugOverlay::show_debug_window;
|
||||
}
|
||||
break;
|
||||
|
||||
case SDL_EVENT_MOUSE_MOTION:
|
||||
break;
|
||||
|
||||
case SDL_EVENT_MOUSE_BUTTON_DOWN:
|
||||
if (event->button.button == SDL_BUTTON_LEFT)
|
||||
{
|
||||
device->HandleTap(&event->button);
|
||||
}
|
||||
break;
|
||||
|
||||
case SDL_EVENT_MOUSE_BUTTON_UP:
|
||||
if (event->button.button == SDL_BUTTON_LEFT)
|
||||
{
|
||||
device->ReleaseTap(&event->button);
|
||||
}
|
||||
|
||||
default: {
|
||||
if (DebugOverlay::show_unhandled_events)
|
||||
{
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Unused event: %d", event->type);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// return continue to continue
|
||||
return SDL_APP_CONTINUE;
|
||||
}
|
||||
|
||||
SDL_AppResult SDL_AppIterate(void *appstate)
|
||||
{
|
||||
const auto context = static_cast<AppContext *>(appstate);
|
||||
|
||||
/// render main window
|
||||
SDL_SetRenderDrawColor(context->MainRenderer(), 0, 0, 0, 255);
|
||||
SDL_RenderClear(context->MainRenderer());
|
||||
|
||||
const uint64_t currentTime = SDL_GetTicks();
|
||||
uint64_t dt = 0;
|
||||
|
||||
if (lastFrameTime > 0) {
|
||||
dt = currentTime - lastFrameTime;
|
||||
}
|
||||
lastFrameTime = currentTime;
|
||||
|
||||
for (const auto &widget : widgets)
|
||||
{
|
||||
widget->Render(dt);
|
||||
}
|
||||
DebugOverlay::Render(context);
|
||||
|
||||
SDL_RenderPresent(context->MainRenderer());
|
||||
|
||||
/// render led matrix
|
||||
context->Render();
|
||||
|
||||
return SDL_APP_CONTINUE;
|
||||
}
|
||||
|
||||
void SDL_AppQuit(void *appstate, SDL_AppResult result)
|
||||
{
|
||||
DebugOverlay::Cleanup();
|
||||
|
||||
free(appstate);
|
||||
|
||||
// SDL will clean up the window/renderer for us.
|
||||
TTF_Quit();
|
||||
}
|
108
firmware/src/manager/ResourceManager.cpp
Normal file
108
firmware/src/manager/ResourceManager.cpp
Normal file
@@ -0,0 +1,108 @@
|
||||
#include "ResourceManager.h"
|
||||
|
||||
#include <SDL3_image/SDL_image.h>
|
||||
#include <memory>
|
||||
#include <ranges>
|
||||
|
||||
ResourceManager &ResourceManager::Instance()
|
||||
{
|
||||
static ResourceManager instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
ResourceManager::ResourceManager()
|
||||
{
|
||||
SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "ResourceManager instance created.");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Destructor for the ResourceManager
|
||||
* Cleans up all loaded textures and frees memory
|
||||
*/
|
||||
ResourceManager::~ResourceManager()
|
||||
{
|
||||
for (const auto &texture : m_textures | std::views::values)
|
||||
{
|
||||
if (texture != nullptr)
|
||||
{
|
||||
SDL_DestroyTexture(texture);
|
||||
}
|
||||
}
|
||||
m_textures.clear();
|
||||
}
|
||||
|
||||
std::string ResourceManager::GetResourcePath(const std::string &fileName)
|
||||
{
|
||||
const auto basePath_c = SDL_GetBasePath();
|
||||
if (!basePath_c)
|
||||
{
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Error retrieving base path: %s", SDL_GetError());
|
||||
// Fallback to relative path as a last resort
|
||||
// Warning: This only works reliably if the working directory is correct
|
||||
// (e.g., when debugging from IDE)
|
||||
return fileName; // Fallback (unsafe)
|
||||
}
|
||||
|
||||
const std::string basePath(basePath_c);
|
||||
|
||||
std::string fullPath = basePath;
|
||||
#ifdef __APPLE__
|
||||
// Special handling for macOS bundle structure
|
||||
// Navigate from /Contents/MacOS/ to /Contents/Resources/
|
||||
size_t lastSlash = fullPath.find_last_of('/');
|
||||
if (lastSlash != std::string::npos)
|
||||
{
|
||||
fullPath = fullPath.substr(0, lastSlash); // Remove executable name
|
||||
lastSlash = fullPath.find_last_of('/');
|
||||
if (lastSlash != std::string::npos)
|
||||
{
|
||||
fullPath = fullPath.substr(0, lastSlash + 1); // Go to Contents folder
|
||||
fullPath += "Resources/";
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback if structure is unexpected
|
||||
fullPath = basePath; // Use original base path
|
||||
fullPath += "Resources/"; // And try with this
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback for unexpected path structure
|
||||
fullPath = basePath;
|
||||
fullPath += "Resources/";
|
||||
}
|
||||
#else
|
||||
// For other platforms (Windows, Linux, etc.)
|
||||
// Use standard assets folder
|
||||
fullPath += "assets/";
|
||||
#endif
|
||||
|
||||
return fullPath + fileName;
|
||||
}
|
||||
|
||||
|
||||
SDL_Texture *ResourceManager::GetTextureByName(SDL_Renderer *renderer, const std::string &path)
|
||||
{
|
||||
std::lock_guard lock(m_mutex);
|
||||
|
||||
// Check if texture is already in cache
|
||||
if (const auto search = m_textures.find(GetResourcePath(path)); search != m_textures.end())
|
||||
{
|
||||
return search->second;
|
||||
}
|
||||
|
||||
// Load new texture
|
||||
SDL_Texture *texture = IMG_LoadTexture(renderer, GetResourcePath(path).c_str());
|
||||
|
||||
if (!texture)
|
||||
{
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Could not load %s -> %s", GetResourcePath(path).c_str(),
|
||||
SDL_GetError());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Store texture in cache
|
||||
m_textures[path] = texture;
|
||||
return texture;
|
||||
}
|
59
firmware/src/manager/ResourceManager.h
Normal file
59
firmware/src/manager/ResourceManager.h
Normal file
@@ -0,0 +1,59 @@
|
||||
#pragma once
|
||||
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
class ResourceManager
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* @brief Returns the singleton instance of the ResourceManager
|
||||
* @return Reference to the ResourceManager instance
|
||||
*/
|
||||
static ResourceManager &Instance();
|
||||
|
||||
ResourceManager(const ResourceManager &) = delete;
|
||||
ResourceManager &operator=(const ResourceManager &) = delete;
|
||||
ResourceManager(ResourceManager &&) = delete;
|
||||
ResourceManager &operator=(ResourceManager &&) = delete;
|
||||
|
||||
~ResourceManager();
|
||||
|
||||
/**
|
||||
* @brief Loads a texture or returns an already loaded texture
|
||||
* @param renderer The SDL_Renderer used to load the texture
|
||||
* @param path Path to the texture file (relative to resource directory)
|
||||
* @return Pointer to the loaded SDL_Texture or nullptr on error
|
||||
*
|
||||
* This function implements a caching system for textures. If a texture
|
||||
* has already been loaded, it returns it from the cache. Otherwise,
|
||||
* it loads the texture and stores it in the cache. Access is thread-safe
|
||||
* through mutex protection.
|
||||
*/
|
||||
SDL_Texture *GetTextureByName(SDL_Renderer *renderer, const std::string &path);
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief Constructor for the ResourceManager
|
||||
* Initializes a new instance and logs its creation
|
||||
*/
|
||||
ResourceManager();
|
||||
|
||||
/**
|
||||
* @brief Determines the full resource path for a file
|
||||
* @param fileName Name of the resource file
|
||||
* @return Complete path to the resource
|
||||
*
|
||||
* This function handles platform-specific paths (especially macOS) and
|
||||
* constructs the correct path to resources. For macOS, it considers the
|
||||
* bundle structure (.app/Contents/Resources/).
|
||||
*/
|
||||
static std::string GetResourcePath(const std::string &fileName);
|
||||
|
||||
std::unordered_map<std::string, SDL_Texture *> m_textures;
|
||||
|
||||
mutable std::mutex m_mutex;
|
||||
};
|
54
firmware/src/model/AppContext.cpp
Normal file
54
firmware/src/model/AppContext.cpp
Normal file
@@ -0,0 +1,54 @@
|
||||
#include "model/AppContext.h"
|
||||
|
||||
#include "Matrix.h"
|
||||
|
||||
auto AppContext::MainWindow() const -> SDL_Window *
|
||||
{
|
||||
return m_window->window();
|
||||
}
|
||||
|
||||
auto AppContext::MainRenderer() const -> SDL_Renderer *
|
||||
{
|
||||
return m_window->renderer();
|
||||
}
|
||||
|
||||
auto AppContext::MainSurface() const -> SDL_Surface *
|
||||
{
|
||||
return SDL_GetWindowSurface(m_window->window());
|
||||
}
|
||||
|
||||
void AppContext::SetMatrix(Matrix *matrix)
|
||||
{
|
||||
m_matrix = matrix;
|
||||
}
|
||||
|
||||
auto AppContext::LedMatrix() const -> Matrix *
|
||||
{
|
||||
return m_matrix;
|
||||
}
|
||||
|
||||
auto AppContext::LedMatrixId() const -> SDL_WindowID
|
||||
{
|
||||
if (m_matrix)
|
||||
{
|
||||
return m_matrix->windowId();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
auto AppContext::LedMatrixRenderer() const -> SDL_Renderer *
|
||||
{
|
||||
if (m_matrix && m_matrix->renderer())
|
||||
{
|
||||
return m_matrix->renderer();
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void AppContext::Render() const
|
||||
{
|
||||
if (m_matrix && m_matrix->renderer())
|
||||
{
|
||||
m_matrix->Render();
|
||||
}
|
||||
}
|
45
firmware/src/model/AppContext.h
Normal file
45
firmware/src/model/AppContext.h
Normal file
@@ -0,0 +1,45 @@
|
||||
#pragma once
|
||||
|
||||
#include "SDL3_ttf/SDL_ttf.h"
|
||||
#include "Window.h"
|
||||
|
||||
class Matrix;
|
||||
|
||||
class AppContext
|
||||
{
|
||||
public:
|
||||
explicit AppContext(const Window *window) : m_window(window)
|
||||
{
|
||||
m_font_default = TTF_OpenFont("haxrcorp-4089.otf", 21);
|
||||
m_font_text = TTF_OpenFont("Helvetica-Bold.otf", 21);
|
||||
}
|
||||
|
||||
~AppContext()
|
||||
{
|
||||
TTF_CloseFont(m_font_default);
|
||||
TTF_CloseFont(m_font_text);
|
||||
}
|
||||
|
||||
[[nodiscard]] auto MainWindow() const -> SDL_Window *;
|
||||
|
||||
[[nodiscard]] auto MainRenderer() const -> SDL_Renderer *;
|
||||
|
||||
[[nodiscard]] auto MainSurface() const -> SDL_Surface *;
|
||||
|
||||
void SetMatrix(Matrix *matrix);
|
||||
|
||||
[[nodiscard]] auto LedMatrix() const -> Matrix *;
|
||||
|
||||
[[nodiscard]] auto LedMatrixId() const -> SDL_WindowID;
|
||||
|
||||
[[nodiscard]] auto LedMatrixRenderer() const -> SDL_Renderer *;
|
||||
|
||||
void Render() const;
|
||||
|
||||
TTF_Font *m_font_default = nullptr;
|
||||
|
||||
private:
|
||||
const Window *m_window;
|
||||
Matrix *m_matrix = nullptr;
|
||||
TTF_Font *m_font_text = nullptr;
|
||||
};
|
11
firmware/src/model/Window.cpp
Normal file
11
firmware/src/model/Window.cpp
Normal file
@@ -0,0 +1,11 @@
|
||||
#include "model/Window.h"
|
||||
|
||||
auto Window::window() const -> SDL_Window *
|
||||
{
|
||||
return m_window;
|
||||
}
|
||||
|
||||
auto Window::renderer() const -> SDL_Renderer *
|
||||
{
|
||||
return SDL_GetRenderer(m_window);
|
||||
}
|
18
firmware/src/model/Window.h
Normal file
18
firmware/src/model/Window.h
Normal file
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include "SDL3/SDL.h"
|
||||
|
||||
class Window
|
||||
{
|
||||
public:
|
||||
explicit Window(SDL_Window *window) : m_window(window)
|
||||
{
|
||||
}
|
||||
|
||||
[[nodiscard]] auto window() const -> SDL_Window *;
|
||||
|
||||
[[nodiscard]] auto renderer() const -> SDL_Renderer *;
|
||||
|
||||
private:
|
||||
SDL_Window *m_window = nullptr;
|
||||
};
|
286
firmware/src/ui/Device.cpp
Normal file
286
firmware/src/ui/Device.cpp
Normal file
@@ -0,0 +1,286 @@
|
||||
#include "ui/Device.h"
|
||||
|
||||
#include <hal/u8g2_hal_sdl.h>
|
||||
#include <u8g2.h>
|
||||
|
||||
#include "MenuOptions.h"
|
||||
#include "common/InactivityTracker.h"
|
||||
#include "hal_native/PersistenceManager.h"
|
||||
#include "ui/ScreenSaver.h"
|
||||
#include "ui/SplashScreen.h"
|
||||
#include "ui/widgets/Button.h"
|
||||
#include "ui/widgets/D_Pad.h"
|
||||
|
||||
u8g2_t u8g2;
|
||||
menu_options_t options;
|
||||
std::unique_ptr<InactivityTracker> m_inactivityTracker;
|
||||
|
||||
static void set_pixel_rgba(const SDL_Surface *surface, const int x, const int y, const uint32_t pixel_color)
|
||||
{
|
||||
if (!surface || x < 0 || x >= surface->w || y < 0 || y >= surface->h)
|
||||
{
|
||||
return;
|
||||
}
|
||||
const auto p = static_cast<uint8_t *>(surface->pixels) + y * surface->pitch + x * sizeof(uint32_t);
|
||||
*reinterpret_cast<uint32_t *>(p) = pixel_color;
|
||||
}
|
||||
|
||||
Device::Device(void *appstate) : UIWidget(appstate)
|
||||
{
|
||||
auto dpad_callback = [](const D_Pad::Direction direction) {
|
||||
SDL_Keycode key = 0;
|
||||
switch (direction)
|
||||
{
|
||||
case D_Pad::Direction::UP:
|
||||
key = SDLK_UP;
|
||||
break;
|
||||
case D_Pad::Direction::DOWN:
|
||||
key = SDLK_DOWN;
|
||||
break;
|
||||
case D_Pad::Direction::LEFT:
|
||||
key = SDLK_LEFT;
|
||||
break;
|
||||
case D_Pad::Direction::RIGHT:
|
||||
key = SDLK_RIGHT;
|
||||
break;
|
||||
case D_Pad::Direction::NONE: // Fallthrough oder keine Aktion
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (key != 0)
|
||||
{
|
||||
PushKey(key);
|
||||
}
|
||||
};
|
||||
|
||||
m_children.push_back(std::make_shared<Button>(
|
||||
GetContext(), U8G2_SCREEN_WIDTH * U8G2_SCREEN_FACTOR + 3 * U8G2_SCREEN_PADDING + DPAD_WIDTH,
|
||||
U8G2_SCREEN_HEIGHT * U8G2_SCREEN_FACTOR + U8G2_SCREEN_PADDING - BUTTON_WIDTH, BUTTON_WIDTH,
|
||||
[]() {
|
||||
PushKey(SDLK_RETURN);
|
||||
}));
|
||||
m_children.push_back(std::make_shared<Button>(
|
||||
GetContext(), U8G2_SCREEN_WIDTH * U8G2_SCREEN_FACTOR + 4 * U8G2_SCREEN_PADDING + DPAD_WIDTH + BUTTON_WIDTH,
|
||||
U8G2_SCREEN_HEIGHT * U8G2_SCREEN_FACTOR + U8G2_SCREEN_PADDING - 2 * BUTTON_WIDTH, BUTTON_WIDTH,
|
||||
[]() {
|
||||
PushKey(SDLK_BACKSPACE);
|
||||
}));
|
||||
m_children.push_back(std::make_shared<D_Pad>(
|
||||
GetContext(), U8G2_SCREEN_WIDTH * U8G2_SCREEN_FACTOR + 2 * U8G2_SCREEN_PADDING,
|
||||
U8G2_SCREEN_HEIGHT * U8G2_SCREEN_FACTOR + U8G2_SCREEN_PADDING - DPAD_WIDTH, DPAD_WIDTH, dpad_callback));
|
||||
|
||||
u8g2_Setup_sh1106_128x64_noname_f(&u8g2, U8G2_R0, u8x8_byte_sdl_hw_spi, u8x8_gpio_and_delay_sdl);
|
||||
u8x8_InitDisplay(u8g2_GetU8x8(&u8g2));
|
||||
|
||||
options = {
|
||||
.u8g2 = &u8g2,
|
||||
.setScreen = [this](const std::shared_ptr<Widget> &screen) {
|
||||
this->SetScreen(screen);
|
||||
},
|
||||
.pushScreen = [this](const std::shared_ptr<Widget> &screen) {
|
||||
this->PushScreen(screen);
|
||||
},
|
||||
.popScreen = [this]() {
|
||||
this->PopScreen();
|
||||
},
|
||||
.persistenceManager = std::make_shared<PersistenceManager>(),
|
||||
};
|
||||
options.persistenceManager->Load();
|
||||
|
||||
m_widget = std::make_shared<SplashScreen>(&options);
|
||||
m_inactivityTracker = std::make_unique<InactivityTracker>(60000, []() {
|
||||
const auto screensaver = std::make_shared<ScreenSaver>(&options);
|
||||
options.pushScreen(screensaver);
|
||||
});
|
||||
}
|
||||
|
||||
void Device::SetScreen(const std::shared_ptr<Widget> &screen)
|
||||
{
|
||||
if (screen != nullptr)
|
||||
{
|
||||
m_widget = screen;
|
||||
m_history.clear();
|
||||
m_history.emplace_back(m_widget);
|
||||
m_widget->enter();
|
||||
}
|
||||
}
|
||||
|
||||
void Device::PushScreen(const std::shared_ptr<Widget> &screen)
|
||||
{
|
||||
if (screen != nullptr)
|
||||
{
|
||||
if (m_widget)
|
||||
{
|
||||
m_widget->pause();
|
||||
}
|
||||
m_widget = screen;
|
||||
m_history.emplace_back(m_widget);
|
||||
m_widget->enter();
|
||||
}
|
||||
}
|
||||
|
||||
void Device::PopScreen()
|
||||
{
|
||||
if (m_history.size() >= 2)
|
||||
{
|
||||
if (m_widget)
|
||||
{
|
||||
m_widget->exit();
|
||||
}
|
||||
m_history.pop_back();
|
||||
m_widget = m_history.back();
|
||||
m_widget->resume();
|
||||
}
|
||||
}
|
||||
|
||||
void Device::PushKey(const SDL_Keycode key)
|
||||
{
|
||||
SDL_Event ev;
|
||||
ev.type = SDL_EVENT_KEY_DOWN;
|
||||
ev.key.key = key;
|
||||
SDL_PushEvent(&ev);
|
||||
}
|
||||
|
||||
void Device::RenderU8G2() const
|
||||
{
|
||||
SDL_Surface *u8g2_surface = nullptr;
|
||||
SDL_Texture *u8g2_texture = nullptr;
|
||||
|
||||
u8g2_surface = SDL_CreateSurface(U8G2_SCREEN_WIDTH, U8G2_SCREEN_HEIGHT, SDL_PIXELFORMAT_RGBA8888);
|
||||
|
||||
if (!u8g2_surface)
|
||||
{
|
||||
SDL_Log("SDL_CreateSurfaceFrom Error: %s\n", SDL_GetError());
|
||||
}
|
||||
else
|
||||
{
|
||||
const auto color_black = SDL_MapSurfaceRGBA(u8g2_surface, 0, 0, 0, 255);
|
||||
const auto color_white = SDL_MapSurfaceRGBA(u8g2_surface, 255, 255, 255, 255);
|
||||
|
||||
const auto u8g2_buf = u8g2_GetBufferPtr(&u8g2);
|
||||
for (auto y = 0; y < U8G2_SCREEN_HEIGHT; ++y)
|
||||
{
|
||||
for (auto x = 0; x < U8G2_SCREEN_WIDTH; ++x)
|
||||
{
|
||||
const auto page = y / 8;
|
||||
const auto bit_index = y % 8;
|
||||
const auto byte_ptr = u8g2_buf + page * U8G2_SCREEN_WIDTH + x;
|
||||
const auto pixel_is_set = (*byte_ptr >> bit_index) & 0x01;
|
||||
set_pixel_rgba(u8g2_surface, x, y, pixel_is_set ? color_white : color_black);
|
||||
}
|
||||
}
|
||||
|
||||
u8g2_texture = SDL_CreateTextureFromSurface(GetContext()->MainRenderer(), u8g2_surface);
|
||||
if (!u8g2_texture)
|
||||
{
|
||||
SDL_Log("SDL_CreateTextureFromSurface Error: %s\n", SDL_GetError());
|
||||
}
|
||||
if (!SDL_SetTextureScaleMode(u8g2_texture, SDL_SCALEMODE_NEAREST))
|
||||
{
|
||||
SDL_Log("SDL_SetTextureScaleMode Error: %s\n", SDL_GetError());
|
||||
}
|
||||
SDL_DestroySurface(u8g2_surface);
|
||||
}
|
||||
|
||||
if (u8g2_texture)
|
||||
{
|
||||
SDL_FRect destRect;
|
||||
destRect.x = U8G2_SCREEN_PADDING;
|
||||
destRect.y = U8G2_SCREEN_PADDING;
|
||||
destRect.w = U8G2_SCREEN_WIDTH * U8G2_SCREEN_FACTOR;
|
||||
destRect.h = U8G2_SCREEN_HEIGHT * U8G2_SCREEN_FACTOR;
|
||||
|
||||
SDL_RenderTexture(GetContext()->MainRenderer(), u8g2_texture, nullptr, &destRect);
|
||||
SDL_DestroyTexture(u8g2_texture);
|
||||
}
|
||||
}
|
||||
|
||||
void Device::DrawBackground() const
|
||||
{
|
||||
int windowWidth = 0;
|
||||
int windowHeight = 0;
|
||||
|
||||
if (!SDL_GetWindowSize(GetContext()->MainWindow(), &windowWidth, &windowHeight))
|
||||
{
|
||||
SDL_Log("SDL_GetWindowSize Error: %s\n", SDL_GetError());
|
||||
}
|
||||
const auto rect = SDL_FRect{0.0f, 0.0f, static_cast<float>(windowWidth), static_cast<float>(windowHeight)};
|
||||
SDL_SetRenderDrawColor(GetContext()->MainRenderer(), 193, 46, 31, 255);
|
||||
SDL_RenderFillRect(GetContext()->MainRenderer(), &rect);
|
||||
}
|
||||
|
||||
void Device::DrawScreen(const uint64_t dt) const
|
||||
{
|
||||
u8g2_ClearBuffer(&u8g2);
|
||||
|
||||
if (m_widget != nullptr)
|
||||
{
|
||||
m_widget->update(dt);
|
||||
m_widget->render();
|
||||
}
|
||||
|
||||
RenderU8G2();
|
||||
}
|
||||
|
||||
void Device::Render(const uint64_t dt) const
|
||||
{
|
||||
DrawBackground();
|
||||
DrawScreen(dt);
|
||||
|
||||
for (const auto &child : m_children)
|
||||
{
|
||||
child->Render(dt);
|
||||
}
|
||||
|
||||
m_inactivityTracker->update(dt);
|
||||
}
|
||||
|
||||
void Device::HandleTap(const SDL_MouseButtonEvent *event) const
|
||||
{
|
||||
// SDL_Log("HandleTap: x=%f, y=%f, button=%d", event->x, event->y, event->button);
|
||||
|
||||
for (const auto &child : m_children)
|
||||
{
|
||||
if (child->IsHit(static_cast<int>(event->x), static_cast<int>(event->y)))
|
||||
{
|
||||
child->OnTap(static_cast<int>(event->x), static_cast<int>(event->y));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Device::ReleaseTap(const SDL_MouseButtonEvent *event) const
|
||||
{
|
||||
// SDL_Log("ReleaseTap: x=%f, y=%f, button=%d", event->x, event->y, event->button);
|
||||
m_inactivityTracker->reset();
|
||||
|
||||
for (const auto &child : m_children)
|
||||
{
|
||||
child->ReleaseTap(static_cast<int>(event->x), static_cast<int>(event->y));
|
||||
}
|
||||
}
|
||||
|
||||
void Device::OnButtonClicked(const ButtonType button) const
|
||||
{
|
||||
m_inactivityTracker->reset();
|
||||
|
||||
if (m_widget != nullptr)
|
||||
{
|
||||
m_widget->onButtonClicked(button);
|
||||
}
|
||||
}
|
||||
|
||||
bool Device::IsHit(int mouse_x, int mouse_y) const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
void Device::OnTap(int mouse_x, int mouse_y)
|
||||
{
|
||||
////
|
||||
}
|
||||
|
||||
void Device::ReleaseTap(int mouse_x, int mouse_y)
|
||||
{
|
||||
///
|
||||
}
|
49
firmware/src/ui/Device.h
Normal file
49
firmware/src/ui/Device.h
Normal file
@@ -0,0 +1,49 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "UIWidget.h"
|
||||
#include "common/Common.h"
|
||||
#include "common/Widget.h"
|
||||
#include "model/AppContext.h"
|
||||
|
||||
class Device final : public UIWidget
|
||||
{
|
||||
public:
|
||||
explicit Device(void *appstate);
|
||||
|
||||
void Render(uint64_t dt) const override;
|
||||
|
||||
void HandleTap(const SDL_MouseButtonEvent *event) const;
|
||||
|
||||
void ReleaseTap(const SDL_MouseButtonEvent *event) const;
|
||||
|
||||
void OnButtonClicked(ButtonType button) const;
|
||||
|
||||
[[nodiscard]] bool IsHit(int mouse_x, int mouse_y) const override;
|
||||
|
||||
void OnTap(int mouse_x, int mouse_y) override;
|
||||
|
||||
void ReleaseTap(int mouse_x, int mouse_y) override;
|
||||
|
||||
private:
|
||||
void DrawBackground() const;
|
||||
|
||||
void DrawScreen(uint64_t dt) const;
|
||||
|
||||
void RenderU8G2() const;
|
||||
|
||||
void SetScreen(const std::shared_ptr<Widget> &screen);
|
||||
|
||||
void PushScreen(const std::shared_ptr<Widget> &screen);
|
||||
|
||||
void PopScreen();
|
||||
|
||||
static void PushKey(SDL_Keycode key);
|
||||
|
||||
std::vector<std::shared_ptr<UIWidget>> m_children{};
|
||||
std::shared_ptr<Widget> m_widget;
|
||||
std::vector<std::shared_ptr<Widget>> m_history;
|
||||
};
|
12
firmware/src/ui/UIWidget.cpp
Normal file
12
firmware/src/ui/UIWidget.cpp
Normal file
@@ -0,0 +1,12 @@
|
||||
#include "ui/UIWidget.h"
|
||||
|
||||
UIWidget::UIWidget(void *appstate) : m_context(static_cast<AppContext *>(appstate))
|
||||
{
|
||||
}
|
||||
|
||||
UIWidget::~UIWidget() = default;
|
||||
|
||||
auto UIWidget::GetContext() const -> AppContext *
|
||||
{
|
||||
return m_context;
|
||||
}
|
24
firmware/src/ui/UIWidget.h
Normal file
24
firmware/src/ui/UIWidget.h
Normal file
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include "model/AppContext.h"
|
||||
|
||||
class UIWidget
|
||||
{
|
||||
public:
|
||||
explicit UIWidget(void *appstate);
|
||||
|
||||
virtual ~UIWidget();
|
||||
|
||||
virtual void Render(uint64_t dt) const = 0;
|
||||
|
||||
[[nodiscard]] virtual bool IsHit(int mouse_x, int mouse_y) const = 0;
|
||||
|
||||
virtual void OnTap(int mouse_x, int mouse_y) = 0;
|
||||
|
||||
virtual void ReleaseTap(int mouse_x, int mouse_y) = 0;
|
||||
|
||||
[[nodiscard]] AppContext *GetContext() const;
|
||||
|
||||
private:
|
||||
AppContext *m_context{};
|
||||
};
|
49
firmware/src/ui/widgets/Button.cpp
Normal file
49
firmware/src/ui/widgets/Button.cpp
Normal file
@@ -0,0 +1,49 @@
|
||||
#include "ui/widgets/Button.h"
|
||||
|
||||
#include "../../manager/ResourceManager.h"
|
||||
#include <functional>
|
||||
#include <utility>
|
||||
|
||||
Button::Button(void *appState, const float x, const float y, const float width, std::function<void()> callback)
|
||||
: UIWidget(appState), m_x(x), m_y(y), m_width(width), m_callback(std::move(callback))
|
||||
{
|
||||
}
|
||||
|
||||
void Button::Render(const uint64_t dt) const
|
||||
{
|
||||
const auto button =
|
||||
ResourceManager::Instance().GetTextureByName(GetContext()->MainRenderer(), "button_normal.png");
|
||||
const auto dst = SDL_FRect(m_x, m_y, m_width, m_width);
|
||||
SDL_RenderTexture(GetContext()->MainRenderer(), button, nullptr, &dst);
|
||||
SDL_DestroyTexture(button);
|
||||
|
||||
if (m_isPressed)
|
||||
{
|
||||
const auto overlay =
|
||||
ResourceManager::Instance().GetTextureByName(GetContext()->MainRenderer(), "button_pressed_overlay.png");
|
||||
SDL_RenderTexture(GetContext()->MainRenderer(), overlay, nullptr, &dst);
|
||||
SDL_DestroyTexture(overlay);
|
||||
}
|
||||
}
|
||||
|
||||
bool Button::IsHit(const int mouse_x, const int mouse_y) const
|
||||
{
|
||||
const auto fx = static_cast<float>(mouse_x);
|
||||
const auto fy = static_cast<float>(mouse_y);
|
||||
return (fx >= m_x && fx <= (m_x + m_width) &&
|
||||
fy >= m_y && fy <= (m_y + m_width)) || m_isPressed;
|
||||
}
|
||||
|
||||
void Button::OnTap(int mouse_x, int mouse_y)
|
||||
{
|
||||
if (m_callback)
|
||||
{
|
||||
m_isPressed = true;
|
||||
m_callback();
|
||||
}
|
||||
}
|
||||
|
||||
void Button::ReleaseTap(int mouse_x, int mouse_y)
|
||||
{
|
||||
m_isPressed = false;
|
||||
}
|
28
firmware/src/ui/widgets/Button.h
Normal file
28
firmware/src/ui/widgets/Button.h
Normal file
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include "ui/UIWidget.h"
|
||||
|
||||
#include <functional>
|
||||
|
||||
#define BUTTON_WIDTH (35)
|
||||
|
||||
class Button final : public UIWidget
|
||||
{
|
||||
public:
|
||||
Button(void *appState, float x, float y, float width, std::function<void()> callback);
|
||||
|
||||
void Render(uint64_t dt) const override;
|
||||
|
||||
[[nodiscard]] bool IsHit(int mouse_x, int mouse_y) const override;
|
||||
|
||||
void OnTap(int mouse_x, int mouse_y) override;
|
||||
|
||||
void ReleaseTap(int mouse_x, int mouse_y) override;
|
||||
|
||||
private:
|
||||
float m_x;
|
||||
float m_y;
|
||||
float m_width;
|
||||
std::function<void()> m_callback;
|
||||
bool m_isPressed = false;
|
||||
};
|
112
firmware/src/ui/widgets/D_Pad.cpp
Normal file
112
firmware/src/ui/widgets/D_Pad.cpp
Normal file
@@ -0,0 +1,112 @@
|
||||
#include "D_Pad.h"
|
||||
#include "ui/widgets/D_Pad.h"
|
||||
|
||||
#include "../../manager/ResourceManager.h"
|
||||
|
||||
D_Pad::D_Pad(void *appState, const float x, const float y, const float width, std::function<void(Direction)> callback)
|
||||
: UIWidget(appState), m_x(x), m_y(y), m_width(width), m_callback(std::move(callback))
|
||||
{
|
||||
}
|
||||
|
||||
void D_Pad::Render(const uint64_t dt) const
|
||||
{
|
||||
const auto dPad =
|
||||
ResourceManager::Instance().GetTextureByName(GetContext()->MainRenderer(), "d-pad_normal.png");
|
||||
|
||||
auto dst = SDL_FRect(m_x, m_y, m_width, m_width);
|
||||
SDL_RenderTexture(GetContext()->MainRenderer(), dPad, nullptr, &dst);
|
||||
SDL_DestroyTexture(dPad);
|
||||
|
||||
if (m_isPressed != Direction::NONE)
|
||||
{
|
||||
const auto overlay =
|
||||
ResourceManager::Instance().GetTextureByName(GetContext()->MainRenderer(), "button_pressed_overlay.png");
|
||||
switch (m_isPressed)
|
||||
{
|
||||
case Direction::UP:
|
||||
dst = SDL_FRect(m_x + m_width / 3.0f, m_y, m_width / 3.0f, m_width / 3.0f);
|
||||
break;
|
||||
|
||||
case Direction::DOWN:
|
||||
dst = SDL_FRect(m_x + m_width / 3.0f, m_y + 2 * m_width / 3.0f, m_width / 3.0f, m_width / 3.0f);
|
||||
break;
|
||||
|
||||
case Direction::LEFT:
|
||||
dst = SDL_FRect(m_x, m_y + m_width / 3.0f, m_width / 3.0f, m_width / 3.0f);
|
||||
break;
|
||||
|
||||
case Direction::RIGHT:
|
||||
dst = SDL_FRect(m_x + 2 * m_width / 3.0f, m_y + m_width / 3.0f, m_width / 3.0f, m_width / 3.0f);
|
||||
break;
|
||||
|
||||
case Direction::NONE:
|
||||
break;
|
||||
}
|
||||
SDL_RenderTexture(GetContext()->MainRenderer(), overlay, nullptr, &dst);
|
||||
SDL_DestroyTexture(overlay);
|
||||
}
|
||||
}
|
||||
|
||||
bool D_Pad::IsHit(const int mouse_x, const int mouse_y) const
|
||||
{
|
||||
const auto fx = static_cast<float>(mouse_x);
|
||||
const auto fy = static_cast<float>(mouse_y);
|
||||
return (fx >= m_x && fx <= (m_x + m_width) &&
|
||||
fy >= m_y && fy <= (m_y + m_width));
|
||||
}
|
||||
|
||||
D_Pad::Direction D_Pad::GetDirectionFromTap(const float local_x, const float local_y) const
|
||||
{
|
||||
const float segment = m_width / 3.0f;
|
||||
|
||||
int col = -1;
|
||||
if (local_x < segment)
|
||||
col = 0;
|
||||
else if (local_x < 2 * segment)
|
||||
col = 1;
|
||||
else if (local_x <= m_width)
|
||||
col = 2;
|
||||
else
|
||||
return Direction::NONE;
|
||||
|
||||
int row = -1;
|
||||
if (local_y < segment)
|
||||
row = 0;
|
||||
else if (local_y < 2 * segment)
|
||||
row = 1;
|
||||
else if (local_y <= m_width)
|
||||
row = 2;
|
||||
else
|
||||
return Direction::NONE;
|
||||
|
||||
if (col == 1 && row == 0)
|
||||
return Direction::UP;
|
||||
if (col == 1 && row == 2)
|
||||
return Direction::DOWN;
|
||||
if (col == 0 && row == 1)
|
||||
return Direction::LEFT;
|
||||
if (col == 2 && row == 1)
|
||||
return Direction::RIGHT;
|
||||
|
||||
return Direction::NONE;
|
||||
}
|
||||
|
||||
void D_Pad::OnTap(const int mouse_x, const int mouse_y)
|
||||
{
|
||||
if (m_callback)
|
||||
{
|
||||
const auto local_x = static_cast<float>(mouse_x) - m_x;
|
||||
|
||||
if (const auto local_y = static_cast<float>(mouse_y) - m_y;
|
||||
local_x >= 0 && local_x <= m_width && local_y >= 0 && local_y <= m_width)
|
||||
{
|
||||
m_isPressed = GetDirectionFromTap(local_x, local_y);
|
||||
m_callback(m_isPressed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void D_Pad::ReleaseTap(const int mouse_x, const int mouse_y)
|
||||
{
|
||||
m_isPressed = Direction::NONE;
|
||||
}
|
30
firmware/src/ui/widgets/D_Pad.h
Normal file
30
firmware/src/ui/widgets/D_Pad.h
Normal file
@@ -0,0 +1,30 @@
|
||||
#pragma once
|
||||
|
||||
#include "functional"
|
||||
#include "ui/UIWidget.h"
|
||||
|
||||
#define DPAD_WIDTH (105)
|
||||
|
||||
|
||||
class D_Pad final : public UIWidget
|
||||
{
|
||||
public:
|
||||
enum class Direction { NONE, UP, DOWN, LEFT, RIGHT };
|
||||
|
||||
D_Pad(void *appState, float x, float y, float width, std::function<void(Direction)> callback);
|
||||
|
||||
void Render(uint64_t dt) const override;
|
||||
|
||||
[[nodiscard]] bool IsHit(int mouse_x, int mouse_y) const override;
|
||||
|
||||
void OnTap(int mouse_x, int mouse_y) override;
|
||||
|
||||
void ReleaseTap(int mouse_x, int mouse_y) override;
|
||||
|
||||
private:
|
||||
float m_x, m_y, m_width;
|
||||
std::function<void(Direction)> m_callback;
|
||||
Direction m_isPressed = Direction::NONE;
|
||||
|
||||
[[nodiscard]] Direction GetDirectionFromTap(float local_x, float local_y) const;
|
||||
};
|
Reference in New Issue
Block a user