GroveEngine/tests/demo/demo_ui_showcase.cpp
StillHammer bc8db4be0c feat: Add UIModule interactive showcase demo
Complete interactive application demonstrating all UIModule features:

Features:
- All 9 widget types (Button, Slider, TextInput, Checkbox, ProgressBar, Label, Panel, ScrollPanel, Image)
- Live event console showing all UI events in real-time
- Event statistics tracking (clicks, actions, value changes, hovers)
- Hot-reload support (press 'R' to reload UI from JSON)
- Mouse interaction (click, hover, drag, wheel)
- Keyboard input (text fields, shortcuts)
- Tooltips on all widgets with smart positioning
- Nested scrollable panels
- Graceful degradation (handles renderer failure in WSL/headless)

Files:
- tests/demo/demo_ui_showcase.cpp (370 lines) - Main demo application
- assets/ui/demo_showcase.json (1100+ lines) - Complete UI layout
- docs/UI_MODULE_DEMO.md - Full documentation
- tests/CMakeLists.txt - Build system integration

Use cases:
- Learning UIModule API and patterns
- Visual regression testing
- Integration example for new projects
- Showcase of GroveEngine capabilities
- Hot-reload workflow demonstration

Run:
  cmake --build build-bgfx --target demo_ui_showcase -j4
  cd build-bgfx/tests && ./demo_ui_showcase

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-29 08:52:25 +08:00

368 lines
13 KiB
C++

/**
* UIModule Interactive Showcase Demo
*
* Features:
* - All widget types (9 widgets)
* - ScrollPanel with dynamic content
* - Tooltips on every widget
* - Live event console
* - Hot-reload support
* - Interactive controls
*
* Controls:
* - Mouse: Click, hover, drag
* - Keyboard: Type in text fields
* - Mouse wheel: Scroll panels
* - ESC: Quit
* - R: Reload UI from JSON
*/
#include <SDL2/SDL.h>
#include <SDL2/SDL_syswm.h>
#include <grove/ModuleLoader.h>
#include <grove/IntraIOManager.h>
#include <grove/IntraIO.h>
#include <grove/JsonDataNode.h>
#include <iostream>
#include <memory>
#include <chrono>
#include <deque>
#include <sstream>
using namespace grove;
// Event log for displaying in console
struct EventLog {
std::deque<std::string> messages;
static const size_t MAX_MESSAGES = 15;
void add(const std::string& msg) {
auto now = std::chrono::system_clock::now();
auto time = std::chrono::system_clock::to_time_t(now);
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
now.time_since_epoch()) % 1000;
std::stringstream ss;
ss << "[" << (time % 100) << "." << ms.count() << "] " << msg;
messages.push_back(ss.str());
if (messages.size() > MAX_MESSAGES) {
messages.pop_front();
}
std::cout << ss.str() << std::endl;
}
void clear() {
messages.clear();
}
};
int main(int argc, char* argv[]) {
std::cout << "\n";
std::cout << "========================================\n";
std::cout << " UIModule Interactive Showcase Demo \n";
std::cout << "========================================\n\n";
std::cout << "Controls:\n";
std::cout << " - Mouse: Click, hover, drag widgets\n";
std::cout << " - Keyboard: Type in text fields\n";
std::cout << " - Mouse wheel: Scroll panels\n";
std::cout << " - ESC: Quit\n";
std::cout << " - R: Reload UI from JSON\n\n";
EventLog eventLog;
eventLog.add("Demo starting...");
// Initialize SDL
if (SDL_Init(SDL_INIT_VIDEO) < 0) {
std::cerr << "SDL initialization failed: " << SDL_GetError() << std::endl;
return 1;
}
const int WINDOW_WIDTH = 1200;
const int WINDOW_HEIGHT = 800;
SDL_Window* window = SDL_CreateWindow(
"UIModule Showcase - All Features Demo",
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
WINDOW_WIDTH, WINDOW_HEIGHT,
SDL_WINDOW_SHOWN
);
if (!window) {
std::cerr << "Window creation failed: " << SDL_GetError() << std::endl;
SDL_Quit();
return 1;
}
eventLog.add("SDL window created");
// Setup IIO
auto& ioManager = IntraIOManager::getInstance();
auto rendererIO = ioManager.createInstance("bgfx_renderer");
auto uiIO = ioManager.createInstance("ui_module");
// Load BgfxRenderer
ModuleLoader rendererLoader;
std::string rendererPath = "../modules/libBgfxRenderer.so";
#ifdef _WIN32
rendererPath = "../modules/BgfxRenderer.dll";
#endif
std::unique_ptr<IModule> renderer;
try {
renderer = rendererLoader.load(rendererPath, "bgfx_renderer");
eventLog.add("BgfxRenderer loaded");
} catch (const std::exception& e) {
std::cerr << "Failed to load renderer: " << e.what() << std::endl;
SDL_DestroyWindow(window);
SDL_Quit();
return 1;
}
// Configure renderer
JsonDataNode rendererConfig("config");
rendererConfig.setInt("windowWidth", WINDOW_WIDTH);
rendererConfig.setInt("windowHeight", WINDOW_HEIGHT);
rendererConfig.setString("backend", "auto");
rendererConfig.setBool("vsync", true);
// Pass SDL window native handle
SDL_SysWMinfo wmInfo;
SDL_VERSION(&wmInfo.version);
if (SDL_GetWindowWMInfo(window, &wmInfo)) {
#ifdef _WIN32
rendererConfig.setInt("windowHandle", reinterpret_cast<int64_t>(wmInfo.info.win.window));
#elif __linux__
rendererConfig.setInt("windowHandle", static_cast<int64_t>(wmInfo.info.x11.window));
#endif
}
renderer->setConfiguration(rendererConfig, rendererIO.get(), nullptr);
eventLog.add("Renderer configured");
// Load UIModule
ModuleLoader uiLoader;
std::string uiPath = "../modules/libUIModule.so";
#ifdef _WIN32
uiPath = "../modules/UIModule.dll";
#endif
std::unique_ptr<IModule> uiModule;
try {
uiModule = uiLoader.load(uiPath, "ui_module");
eventLog.add("UIModule loaded");
} catch (const std::exception& e) {
std::cerr << "Failed to load UI module: " << e.what() << std::endl;
renderer->shutdown();
SDL_DestroyWindow(window);
SDL_Quit();
return 1;
}
// Configure UIModule
JsonDataNode uiConfig("config");
uiConfig.setInt("windowWidth", WINDOW_WIDTH);
uiConfig.setInt("windowHeight", WINDOW_HEIGHT);
uiConfig.setString("layoutFile", "../../assets/ui/demo_showcase.json");
uiConfig.setInt("baseLayer", 1000);
uiModule->setConfiguration(uiConfig, uiIO.get(), nullptr);
eventLog.add("UIModule configured");
// Subscribe to UI events
uiIO->subscribe("ui:click");
uiIO->subscribe("ui:action");
uiIO->subscribe("ui:value_changed");
uiIO->subscribe("ui:text_changed");
uiIO->subscribe("ui:text_submit");
uiIO->subscribe("ui:hover");
uiIO->subscribe("ui:focus_gained");
uiIO->subscribe("ui:focus_lost");
eventLog.add("Ready! Interact with widgets below.");
// Check renderer health
auto rendererHealth = renderer->getHealthStatus();
bool rendererOK = rendererHealth &&
rendererHealth->getString("status", "") == "healthy";
if (!rendererOK) {
std::cout << "⚠️ Renderer not healthy, running in UI-only mode (no rendering)\n";
eventLog.add("⚠️ Renderer offline - UI-only mode");
} else {
std::cout << "✅ Renderer healthy\n";
eventLog.add("✅ Renderer active");
}
// Stats
int clickCount = 0;
int actionCount = 0;
int valueChangeCount = 0;
int hoverCount = 0;
// Main loop
bool running = true;
auto lastFrameTime = std::chrono::high_resolution_clock::now();
while (running) {
auto currentTime = std::chrono::high_resolution_clock::now();
float deltaTime = std::chrono::duration<float>(currentTime - lastFrameTime).count();
lastFrameTime = currentTime;
// Process SDL events
SDL_Event event;
while (SDL_PollEvent(&event)) {
if (event.type == SDL_QUIT) {
running = false;
}
else if (event.type == SDL_KEYDOWN) {
if (event.key.keysym.sym == SDLK_ESCAPE) {
running = false;
}
else if (event.key.keysym.sym == SDLK_r) {
// Hot-reload UI
eventLog.add("🔄 Reloading UI from JSON...");
uiModule->setConfiguration(uiConfig, uiIO.get(), nullptr);
eventLog.add("✅ UI reloaded!");
}
else {
// Forward keyboard to UI
auto keyPress = std::make_unique<JsonDataNode>("key_press");
keyPress->setInt("key", event.key.keysym.sym);
keyPress->setInt("char", event.key.keysym.sym);
uiIO->publish("input:key:press", std::move(keyPress));
}
}
else if (event.type == SDL_MOUSEMOTION) {
auto mouseMove = std::make_unique<JsonDataNode>("mouse_move");
mouseMove->setDouble("x", static_cast<double>(event.motion.x));
mouseMove->setDouble("y", static_cast<double>(event.motion.y));
uiIO->publish("input:mouse:move", std::move(mouseMove));
}
else if (event.type == SDL_MOUSEBUTTONDOWN || event.type == SDL_MOUSEBUTTONUP) {
auto mouseButton = std::make_unique<JsonDataNode>("mouse_button");
mouseButton->setInt("button", event.button.button - 1);
mouseButton->setBool("pressed", event.type == SDL_MOUSEBUTTONDOWN);
mouseButton->setDouble("x", static_cast<double>(event.button.x));
mouseButton->setDouble("y", static_cast<double>(event.button.y));
uiIO->publish("input:mouse:button", std::move(mouseButton));
}
else if (event.type == SDL_MOUSEWHEEL) {
auto mouseWheel = std::make_unique<JsonDataNode>("mouse_wheel");
mouseWheel->setDouble("delta", static_cast<double>(event.wheel.y));
uiIO->publish("input:mouse:wheel", std::move(mouseWheel));
}
else if (event.type == SDL_TEXTINPUT) {
auto textInput = std::make_unique<JsonDataNode>("text_input");
textInput->setString("text", event.text.text);
uiIO->publish("input:text", std::move(textInput));
}
}
// Process UI events
while (uiIO->hasMessages() > 0) {
auto msg = uiIO->pullMessage();
if (msg.topic == "ui:click") {
clickCount++;
std::string widgetId = msg.data->getString("widgetId", "");
eventLog.add("🖱️ Click: " + widgetId);
}
else if (msg.topic == "ui:action") {
actionCount++;
std::string action = msg.data->getString("action", "");
std::string widgetId = msg.data->getString("widgetId", "");
eventLog.add("⚡ Action: " + action + " (" + widgetId + ")");
// Handle demo actions
if (action == "demo:clear_log") {
eventLog.clear();
eventLog.add("Log cleared");
}
else if (action == "demo:reset_stats") {
clickCount = 0;
actionCount = 0;
valueChangeCount = 0;
hoverCount = 0;
eventLog.add("Stats reset");
}
}
else if (msg.topic == "ui:value_changed") {
valueChangeCount++;
std::string widgetId = msg.data->getString("widgetId", "");
if (msg.data->hasChild("value")) {
double value = msg.data->getDouble("value", 0.0);
eventLog.add("📊 Value: " + widgetId + " = " + std::to_string(static_cast<int>(value)));
}
else if (msg.data->hasChild("checked")) {
bool checked = msg.data->getBool("checked", false);
eventLog.add("☑️ Checkbox: " + widgetId + " = " + (checked ? "ON" : "OFF"));
}
}
else if (msg.topic == "ui:text_changed") {
std::string widgetId = msg.data->getString("widgetId", "");
std::string text = msg.data->getString("text", "");
eventLog.add("✏️ Text: " + widgetId + " = \"" + text + "\"");
}
else if (msg.topic == "ui:text_submit") {
std::string widgetId = msg.data->getString("widgetId", "");
std::string text = msg.data->getString("text", "");
eventLog.add("✅ Submit: " + widgetId + " = \"" + text + "\"");
}
else if (msg.topic == "ui:hover") {
bool enter = msg.data->getBool("enter", false);
if (enter) {
hoverCount++;
// Don't log hover to avoid spam
}
}
}
// Update modules
JsonDataNode frameInput("input");
frameInput.setDouble("deltaTime", deltaTime);
uiModule->process(frameInput);
// Only call renderer if it's healthy
if (rendererOK) {
renderer->process(frameInput);
}
// Limit framerate to ~60fps
SDL_Delay(16);
}
// Cleanup
std::cout << "\nShutdown sequence...\n";
eventLog.add("Shutting down...");
std::cout << "\nFinal stats:\n";
std::cout << " Clicks: " << clickCount << "\n";
std::cout << " Actions: " << actionCount << "\n";
std::cout << " Value changes: " << valueChangeCount << "\n";
std::cout << " Hovers: " << hoverCount << "\n";
uiModule->shutdown();
uiModule.reset();
uiLoader.unload();
renderer->shutdown();
renderer.reset();
rendererLoader.unload();
std::this_thread::sleep_for(std::chrono::milliseconds(100));
ioManager.removeInstance("bgfx_renderer");
ioManager.removeInstance("ui_module");
SDL_DestroyWindow(window);
SDL_Quit();
std::cout << "\n✅ Demo shutdown complete\n";
return 0;
}