From 8de36aa975029b33ee50142c6716df49dabd1683 Mon Sep 17 00:00:00 2001 From: omigamedev Date: Fri, 16 Jan 2026 20:15:34 +0100 Subject: [PATCH] finish the testing framework --- CLAUDE.md | 17 +- MILESTONE-2.md | 55 ++- ROADMAP.md | 36 +- TESTING.md | 145 +++++++- designer/CMakeLists.txt | 4 + designer/src/backend/RmlUi_Backend.h | 75 ++++ .../src/backend/RmlUi_Backend_GLFW_GL3.cpp | 319 ++++++++++++++++ designer/src/backend/RmlUi_Platform_GLFW.cpp | 348 ++++++++++++++++++ designer/src/backend/RmlUi_Platform_GLFW.h | 81 ++++ designer/src/main.cpp | 26 +- 10 files changed, 1044 insertions(+), 62 deletions(-) create mode 100644 designer/src/backend/RmlUi_Backend.h create mode 100644 designer/src/backend/RmlUi_Backend_GLFW_GL3.cpp create mode 100644 designer/src/backend/RmlUi_Platform_GLFW.cpp create mode 100644 designer/src/backend/RmlUi_Platform_GLFW.h diff --git a/CLAUDE.md b/CLAUDE.md index 43c5a46..9686471 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -146,6 +146,8 @@ The desktop designer (`designer/`) provides rapid UI development with: - **UI Hierarchy Dumping**: Exports element tree to JSON for inspection - **Screenshot Capture**: PNG export via F12 key - **Logging**: Detailed output for debugging navigation and events +- **Action Recording**: Record mouse/keyboard interactions to JSON +- **Action Playback**: Replay recorded interactions with timing ### Key Files @@ -154,7 +156,10 @@ The desktop designer (`designer/`) provides rapid UI development with: | `designer/src/main.cpp` | Main entry point, GLFW window, event loop | | `designer/src/desktop_kernel.cpp` | RmlUi context management, rendering | | `designer/src/testing/ui_inspector.cpp` | UI hierarchy JSON export | -| `designer/src/testing/visual_capture.cpp` | PNG screenshot capture | +| `designer/src/testing/visual_capture.cpp` | PNG screenshot capture and comparison | +| `designer/src/testing/action_recorder.cpp` | Record user interactions to JSON | +| `designer/src/testing/action_player.cpp` | Playback recorded actions | +| `designer/src/backend/RmlUi_Backend_GLFW_GL3.cpp` | GLFW backend with input hooks | ### Command Line Options @@ -162,8 +167,18 @@ The desktop designer (`designer/`) provides rapid UI development with: --log Write logs to file --hierarchy Dump UI hierarchy JSON each frame --dump Single-shot dump mode (screenshot + hierarchy) +--record Enable recording mode (F5 to start/stop) +--playback Play back recorded actions from JSON ``` +### Keyboard Controls + +| Key | Function | +|-----|----------| +| F5 | Start/stop recording (when --record is enabled) | +| F6 | Pause/resume playback (when --playback is enabled) | +| F12 | Take screenshot | + ## Automated Testing Framework The designer-test (`designer-test/`) provides automated UI testing: diff --git a/MILESTONE-2.md b/MILESTONE-2.md index 20afc61..8c58e91 100644 --- a/MILESTONE-2.md +++ b/MILESTONE-2.md @@ -1,6 +1,6 @@ # Milestone 2: Testing Framework -**Status**: 95% Complete +**Status**: 100% Complete **Goal**: Automated UI testing for rapid iteration and AI agent verification. --- @@ -95,44 +95,35 @@ mosis-designer.exe home.rml --playback my-test.json --- -## Remaining Task +## Completed: GLFW Input Hooks for Recording -### Task 2.3: GLFW Input Hooks for Recording +### Task 2.3: GLFW Input Hooks -**Status**: Partially Complete -**Effort**: Medium -**Limitation**: Requires RmlUi Backend modification +**Status**: Complete -**Current State**: -- Recording infrastructure is complete (ActionRecorder) -- Playback works fully (ActionPlayer calls RmlUi context directly) -- CLI and keyboard controls are wired up -- **Missing**: Direct GLFW callback access for mouse recording +**Solution Implemented**: +Forked the RmlUi backend files into `designer/src/backend/` with input recording hooks: -**The Problem**: -The RmlUi Backend abstraction handles all GLFW callbacks internally and doesn't expose them for interception. To record actual mouse events, we would need to either: +1. **RmlUi_Backend.h** - Added callback type definitions: + - `MouseButtonCallback` - Called on mouse button press/release + - `MouseMoveCallback` - Called on mouse movement + - `KeyCallback` - Called on key press/release -1. **Modify RmlUi Backend** (third-party code) - - Add callback hooks to `RmlUi_Backend_GLFW_GL3.cpp` - - Expose GLFW window handle for custom callbacks +2. **RmlUi_Backend_GLFW_GL3.cpp** - Modified GLFW callbacks to: + - Track mouse position in framebuffer coordinates + - Call recording callbacks before forwarding to RmlUi + - Support all three callback types -2. **Fork RmlUi Backends** (more maintainable) - - Copy Backend files into designer project - - Add recording hooks +3. **main.cpp** - Connected callbacks to ActionRecorder: + - Mouse button events trigger `RecordMouseDown`/`RecordMouseUp` + - Mouse move events trigger `RecordMouseMove` + - Key events trigger `RecordKey` -3. **Alternative: Element-Based Recording** - - Listen to RmlUi events after processing - - Record element clicks by ID rather than coordinates - - Less precise but avoids backend modification - -**Workaround for Now**: -Tests can be created manually by: -1. Using the UI hierarchy to find element coordinates -2. Writing JSON test files directly -3. Using the external designer-test framework (Windows SendInput) - -**Future Work**: -Consider option 2 (fork backends) when recording becomes a priority. +**Files Added**: +- `designer/src/backend/RmlUi_Backend.h` +- `designer/src/backend/RmlUi_Backend_GLFW_GL3.cpp` +- `designer/src/backend/RmlUi_Platform_GLFW.h` +- `designer/src/backend/RmlUi_Platform_GLFW.cpp` --- diff --git a/ROADMAP.md b/ROADMAP.md index 369c138..47408d7 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -28,7 +28,7 @@ Mosis is a **virtual smartphone OS** for VR games and applications. It provides | # | Milestone | Status | Description | |---|-----------|--------|-------------| | 1 | Cross-Platform Kernel | ✅ Complete | Desktop designer with shared kernel code | -| 2 | Testing Framework | 🔶 80% | Automated UI testing and inspection | +| 2 | Testing Framework | ✅ Complete | Automated UI testing and inspection | | 3 | Virtual Hardware | ❌ Not started | Camera, mic, speaker, filesystem APIs | | 4 | App Sandboxing | ❌ Not started | Lua/WASM runtime, package format | | 5 | WebRTC Bridge | ❌ Not started | Phone-to-phone communication | @@ -59,7 +59,7 @@ Mosis is a **virtual smartphone OS** for VR games and applications. It provides --- -## Milestone 2: Testing Automation 🔶 80% COMPLETE +## Milestone 2: Testing Automation ✅ COMPLETE **Goal**: Automated UI testing for rapid iteration and AI agent verification. @@ -74,23 +74,10 @@ Mosis is a **virtual smartphone OS** for VR games and applications. It provides - LogParser (navigation event verification) - [x] All 5 navigation tests passing - [x] Android event injection via ADB broadcast - -### Remaining Tasks - -- [ ] **Action Recording** - - Capture tap, swipe, long_press to JSON - - Record timestamps for replay timing - - Save to `test-recordings/*.json` - -- [ ] **Action Playback** - - Load recorded JSON - - Replay with timing - - Capture results at each step - -- [ ] **Screenshot Diff** - - Compare two PNG screenshots - - Highlight pixel differences - - Report diff percentage +- [x] **Action Recording** - Capture tap, swipe, long_press to JSON with timestamps +- [x] **Action Playback** - Load recorded JSON and replay with timing +- [x] **Screenshot Diff** - Pixel-level PNG comparison with configurable tolerance +- [x] **GLFW Input Hooks** - Automatic mouse/key recording via forked backend ### Test Recording Format @@ -435,10 +422,11 @@ cmake --build build --config Debug ## Current Sprint: Complete Partial Tasks -### Priority 1: Testing Framework Completion -- [ ] Action recording (capture interactions to JSON) -- [ ] Action playback (replay with timing) -- [ ] Screenshot diff (visual regression) +### Priority 1: Testing Framework Completion ✅ DONE +- [x] Action recording (capture interactions to JSON) +- [x] Action playback (replay with timing) +- [x] Screenshot diff (visual regression) +- [x] GLFW input hooks for automatic recording ### Priority 2: Remaining System Apps - [ ] Store app (UI only - browse, install) @@ -472,4 +460,4 @@ cmake --build build --config Debug --- -*Last updated: 2024-01-16* +*Last updated: 2026-01-16* diff --git a/TESTING.md b/TESTING.md index 6c0e31e..367eda5 100644 --- a/TESTING.md +++ b/TESTING.md @@ -10,6 +10,8 @@ The testing framework enables automated validation of UI behavior through: 2. **Input Simulation**: Mouse clicks via Windows SendInput API 3. **Log Verification**: Check navigation events via log file parsing 4. **Test Results**: JSON output compatible with CI/CD pipelines +5. **Action Recording/Playback**: Record and replay user interactions +6. **Visual Regression**: Screenshot comparison with pixel-level diff ## Components @@ -21,8 +23,22 @@ The desktop designer serves as the test target. When launched with testing optio mosis-designer.exe home.rml --log test.log --hierarchy hierarchy.json ``` -- `--log`: Writes all RmlUi INFO logs to file (navigation events, errors) -- `--hierarchy`: Dumps UI element tree to JSON each frame +**Testing Options**: + +| Option | Description | +|--------|-------------| +| `--log FILE` | Write all RmlUi INFO logs to file (navigation events, errors) | +| `--hierarchy FILE` | Dump UI element tree to JSON each frame | +| `--record FILE` | Enable action recording mode (F5 to start/stop) | +| `--playback FILE` | Play back recorded actions from JSON file | + +**Keyboard Controls**: + +| Key | Function | +|-----|----------| +| F5 | Start/stop recording (when `--record` is enabled) | +| F6 | Pause/resume playback (when `--playback` is enabled) | +| F12 | Take screenshot (saves to current directory) | ### Test Runner (designer-test.exe) @@ -231,6 +247,124 @@ results.SaveToFile(resultsPath); } ``` +## Action Recording and Playback + +The designer supports recording user interactions and playing them back for automated testing. + +### Recording Actions + +```bash +# Start designer with recording enabled +mosis-designer.exe home.rml --record my-test.json + +# Press F5 to start recording +# Interact with the UI (clicks, swipes, etc.) +# Press F5 again to stop and save +``` + +Recording is automatically saved when you close the window. + +### Playing Back Actions + +```bash +# Play back a recorded test +mosis-designer.exe home.rml --playback my-test.json +``` + +Use F6 to pause/resume playback. + +### Action Recording Format + +```json +{ + "name": "Navigate to contacts", + "description": "Test navigation flow", + "screen_width": 540, + "screen_height": 960, + "initial_screen": "apps/home/home.rml", + "actions": [ + {"type": "tap", "x": 413, "y": 1174, "timestamp": 0}, + {"type": "wait", "duration": 1000, "timestamp": 100}, + {"type": "tap", "x": 40, "y": 28, "timestamp": 1100}, + {"type": "swipe", "x1": 100, "y1": 500, "x2": 100, "y2": 200, "duration": 300, "timestamp": 2000} + ] +} +``` + +### Supported Action Types + +| Type | Fields | Description | +|------|--------|-------------| +| `tap` | x, y, timestamp | Single tap at coordinates | +| `swipe` | x1, y1, x2, y2, duration, timestamp | Swipe gesture | +| `long_press` | x, y, duration, timestamp | Long press gesture | +| `button` | button, timestamp | Hardware button ("back", "home") | +| `wait` | duration, timestamp | Pause between actions | +| `key` | key_code, pressed, timestamp | Keyboard input | + +### Creating Test Files Manually + +You can also create test files manually using the UI hierarchy to find element coordinates: + +```bash +# Get element coordinates from hierarchy +mosis-designer.exe home.rml --hierarchy hierarchy.json +# Read hierarchy.json to find element bounds +# Write action JSON with those coordinates +``` + +## Screenshot Comparison + +The testing framework includes pixel-level screenshot comparison for visual regression testing. + +### Using Screenshot Comparison + +```cpp +#include "testing/visual_capture.h" + +// Capture a screenshot +mosis::testing::VisualCapture capture(540, 960); +capture.CaptureScreenshot("current.png"); + +// Compare two screenshots +float diff = mosis::testing::VisualCapture::CompareImages("baseline.png", "current.png"); + +// diff = 0.0 means identical +// diff = 1.0 means completely different +// Typical threshold: diff < 0.01 (less than 1% different) +``` + +### Comparison Details + +- Compares RGBA pixels with a tolerance of 2 per channel +- Returns ratio of differing pixels (0.0 to 1.0) +- Different dimensions = 1.0 (completely different) +- Missing files = 1.0 (comparison failed) + +### Visual Regression Test Example + +```cpp +bool TestVisualRegression(TestContext& ctx) { + // Navigate to screen + GoHome(ctx); + ClickById(ctx, "dock-phone"); + std::this_thread::sleep_for(std::chrono::milliseconds(1000)); + + // Capture screenshot + mosis::testing::VisualCapture capture(ctx.width, ctx.height); + capture.CaptureScreenshot("dialer-current.png"); + + // Compare with baseline + float diff = mosis::testing::VisualCapture::CompareImages( + "baselines/dialer-expected.png", + "dialer-current.png" + ); + + // Allow up to 1% difference + return diff < 0.01f; +} +``` + ## Running Tests ### Command Line @@ -389,9 +523,12 @@ class NavigationTest { ## Future Improvements -- [ ] Action recording (capture user interactions) -- [ ] Screenshot comparison (visual regression testing) +- [x] Action recording (capture user interactions) - *CLI and infrastructure complete* +- [x] Screenshot comparison (visual regression testing) - *Pixel-level diff implemented* +- [x] Action playback with timing - *Fully functional* +- [x] GLFW input hooks for automatic mouse recording - *Complete via forked backend* - [ ] Android instrumentation test suite - [ ] Parallel test execution - [ ] Test coverage reporting - [ ] Cross-platform test runner (desktop + Android) +- [ ] Visual diff output (highlight changed pixels) diff --git a/designer/CMakeLists.txt b/designer/CMakeLists.txt index 66a23e7..56fd14e 100644 --- a/designer/CMakeLists.txt +++ b/designer/CMakeLists.txt @@ -66,11 +66,15 @@ add_executable(mosis-designer src/testing/action_player.cpp src/testing/ui_inspector.cpp src/testing/visual_capture.cpp + # Local backend with input recording hooks + src/backend/RmlUi_Backend_GLFW_GL3.cpp + src/backend/RmlUi_Platform_GLFW.cpp ) target_include_directories(mosis-designer PRIVATE src src/testing + src/backend ../src/main/kernel/include ../src/main/cpp ) diff --git a/designer/src/backend/RmlUi_Backend.h b/designer/src/backend/RmlUi_Backend.h new file mode 100644 index 0000000..8262033 --- /dev/null +++ b/designer/src/backend/RmlUi_Backend.h @@ -0,0 +1,75 @@ +/* + * This source file is part of RmlUi, the HTML/CSS Interface Middleware + * Modified for Mosis Designer to add input recording hooks. + * + * Original copyright (c) 2008-2010 CodePoint Ltd, Shift Technology Ltd + * Original copyright (c) 2019-2023 The RmlUi Team, and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + */ + +#ifndef RMLUI_BACKENDS_BACKEND_H +#define RMLUI_BACKENDS_BACKEND_H + +#include +#include +#include +#include +#include + +using KeyDownCallback = bool (*)(Rml::Context* context, Rml::Input::KeyIdentifier key, int key_modifier, float native_dp_ratio, bool priority); + +// Input recording callbacks (Mosis extension) +using MouseButtonCallback = std::function; +using MouseMoveCallback = std::function; +using KeyCallback = std::function; + +/** + This interface serves as a basic abstraction over the various backends included with RmlUi. + Modified for Mosis Designer to add input recording hooks. + */ +namespace Backend { + +// Initializes the backend, including the custom system and render interfaces, and opens a window for rendering the RmlUi context. +bool Initialize(const char* window_name, int width, int height, bool allow_resize); +// Closes the window and release all resources owned by the backend, including the system and render interfaces. +void Shutdown(); + +// Returns a pointer to the custom system interface which should be provided to RmlUi. +Rml::SystemInterface* GetSystemInterface(); +// Returns a pointer to the custom render interface which should be provided to RmlUi. +Rml::RenderInterface* GetRenderInterface(); + +// Polls and processes events from the current platform, and applies any relevant events to the provided RmlUi context and the key down callback. +// @return False to indicate that the application should be closed. +bool ProcessEvents(Rml::Context* context, KeyDownCallback key_down_callback = nullptr, bool power_save = false); +// Request application closure during the next event processing call. +void RequestExit(); + +// Prepares the render state to accept rendering commands from RmlUi, call before rendering the RmlUi context. +void BeginFrame(); +// Presents the rendered frame to the screen, call after rendering the RmlUi context. +void PresentFrame(); + +// --- Mosis Extension: Input Recording Hooks --- + +// Set callback for mouse button events (called before RmlUi processes the event) +void SetMouseButtonCallback(MouseButtonCallback callback); + +// Set callback for mouse move events (called before RmlUi processes the event) +void SetMouseMoveCallback(MouseMoveCallback callback); + +// Set callback for keyboard events (called before RmlUi processes the event) +void SetKeyCallback(KeyCallback callback); + +} // namespace Backend + +#endif diff --git a/designer/src/backend/RmlUi_Backend_GLFW_GL3.cpp b/designer/src/backend/RmlUi_Backend_GLFW_GL3.cpp new file mode 100644 index 0000000..bd06d72 --- /dev/null +++ b/designer/src/backend/RmlUi_Backend_GLFW_GL3.cpp @@ -0,0 +1,319 @@ +/* + * This source file is part of RmlUi, the HTML/CSS Interface Middleware + * Modified for Mosis Designer to add input recording hooks. + * + * Original copyright (c) 2008-2010 CodePoint Ltd, Shift Technology Ltd + * Original copyright (c) 2019-2023 The RmlUi Team, and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + */ + +#include "RmlUi_Backend.h" +#include "RmlUi_Platform_GLFW.h" +#include "RmlUi_Renderer_GL3.h" +#include +#include +#include +#include +#include + +static void SetupCallbacks(GLFWwindow* window); + +static void LogErrorFromGLFW(int error, const char* description) +{ + Rml::Log::Message(Rml::Log::LT_ERROR, "GLFW error (0x%x): %s", error, description); +} + +/** + Global data used by this backend. + + Lifetime governed by the calls to Backend::Initialize() and Backend::Shutdown(). + */ +struct BackendData { + SystemInterface_GLFW system_interface; + RenderInterface_GL3 render_interface; + GLFWwindow* window = nullptr; + int glfw_active_modifiers = 0; + bool context_dimensions_dirty = true; + + // Arguments set during event processing and nulled otherwise. + Rml::Context* context = nullptr; + KeyDownCallback key_down_callback = nullptr; + + // Current mouse position (for recording) + int mouse_x = 0; + int mouse_y = 0; + + // Mosis extension: Input recording callbacks + MouseButtonCallback mouse_button_callback; + MouseMoveCallback mouse_move_callback; + KeyCallback key_callback; +}; +static Rml::UniquePtr data; + +bool Backend::Initialize(const char* name, int width, int height, bool allow_resize) +{ + RMLUI_ASSERT(!data); + + glfwSetErrorCallback(LogErrorFromGLFW); + + if (!glfwInit()) + return false; + + // Set window hints for OpenGL 3.3 Core context creation. + glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); + glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); + glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); + glfwWindowHint(GLFW_DOUBLEBUFFER, GLFW_TRUE); + + // Apply window properties and create it. + glfwWindowHint(GLFW_RESIZABLE, allow_resize ? GLFW_TRUE : GLFW_FALSE); + glfwWindowHint(GLFW_SCALE_TO_MONITOR, GLFW_TRUE); + + glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); + + GLFWwindow* window = glfwCreateWindow(width, height, name, nullptr, nullptr); + if (!window) + return false; + + glfwMakeContextCurrent(window); + glfwSwapInterval(1); + + // Load the OpenGL functions. + Rml::String renderer_message; + if (!RmlGL3::Initialize(&renderer_message)) + return false; + + // Construct the system and render interface, this includes compiling all the shaders. If this fails, it is likely an error in the shader code. + data = Rml::MakeUnique(); + if (!data || !data->render_interface) + return false; + + data->window = window; + data->system_interface.SetWindow(window); + data->system_interface.LogMessage(Rml::Log::LT_INFO, renderer_message); + + // The window size may have been scaled by DPI settings, get the actual pixel size. + glfwGetFramebufferSize(window, &width, &height); + data->render_interface.SetViewport(width, height); + + // Receive num lock and caps lock modifiers for proper handling of numpad inputs in text fields. + glfwSetInputMode(window, GLFW_LOCK_KEY_MODS, GLFW_TRUE); + + // Setup the input and window event callback functions. + SetupCallbacks(window); + + return true; +} + +void Backend::Shutdown() +{ + RMLUI_ASSERT(data); + glfwDestroyWindow(data->window); + data.reset(); + RmlGL3::Shutdown(); + glfwTerminate(); +} + +Rml::SystemInterface* Backend::GetSystemInterface() +{ + RMLUI_ASSERT(data); + return &data->system_interface; +} + +Rml::RenderInterface* Backend::GetRenderInterface() +{ + RMLUI_ASSERT(data); + return &data->render_interface; +} + +bool Backend::ProcessEvents(Rml::Context* context, KeyDownCallback key_down_callback, bool power_save) +{ + RMLUI_ASSERT(data && context); + + // The initial window size may have been affected by system DPI settings, apply the actual pixel size and dp-ratio to the context. + if (data->context_dimensions_dirty) + { + data->context_dimensions_dirty = false; + + Rml::Vector2i window_size; + float dp_ratio = 1.f; + glfwGetFramebufferSize(data->window, &window_size.x, &window_size.y); + glfwGetWindowContentScale(data->window, &dp_ratio, nullptr); + + context->SetDimensions(window_size); + context->SetDensityIndependentPixelRatio(dp_ratio); + } + + data->context = context; + data->key_down_callback = key_down_callback; + + if (power_save) + glfwWaitEventsTimeout(Rml::Math::Min(context->GetNextUpdateDelay(), 10.0)); + else + glfwPollEvents(); + + data->context = nullptr; + data->key_down_callback = nullptr; + + const bool result = !glfwWindowShouldClose(data->window); + glfwSetWindowShouldClose(data->window, GLFW_FALSE); + return result; +} + +void Backend::RequestExit() +{ + RMLUI_ASSERT(data); + glfwSetWindowShouldClose(data->window, GLFW_TRUE); +} + +void Backend::BeginFrame() +{ + RMLUI_ASSERT(data); + data->render_interface.Clear(); + data->render_interface.BeginFrame(); +} + +void Backend::PresentFrame() +{ + RMLUI_ASSERT(data); + data->render_interface.EndFrame(0); // 0 = default framebuffer + glfwSwapBuffers(data->window); + + // Optional, used to mark frames during performance profiling. + RMLUI_FrameMark; +} + +// --- Mosis Extension: Input Recording Hooks --- + +void Backend::SetMouseButtonCallback(MouseButtonCallback callback) +{ + if (data) + data->mouse_button_callback = std::move(callback); +} + +void Backend::SetMouseMoveCallback(MouseMoveCallback callback) +{ + if (data) + data->mouse_move_callback = std::move(callback); +} + +void Backend::SetKeyCallback(KeyCallback callback) +{ + if (data) + data->key_callback = std::move(callback); +} + +// Helper function to convert GLFW coordinates to framebuffer coordinates +static void ConvertToFramebufferCoords(GLFWwindow* window, double xpos, double ypos, int& out_x, int& out_y) +{ + using Rml::Vector2i; + using Vector2d = Rml::Vector2; + + Vector2i window_size, framebuffer_size; + glfwGetWindowSize(window, &window_size.x, &window_size.y); + glfwGetFramebufferSize(window, &framebuffer_size.x, &framebuffer_size.y); + + const Vector2d mouse_pos = Vector2d(xpos, ypos) * (Vector2d(framebuffer_size) / Vector2d(window_size)); + out_x = int(Rml::Math::Round(mouse_pos.x)); + out_y = int(Rml::Math::Round(mouse_pos.y)); +} + +static void SetupCallbacks(GLFWwindow* window) +{ + RMLUI_ASSERT(data); + + // Key input + glfwSetKeyCallback(window, [](GLFWwindow* /*window*/, int glfw_key, int /*scancode*/, int glfw_action, int glfw_mods) { + if (!data->context) + return; + + // Store the active modifiers for later because GLFW doesn't provide them in the callbacks to the mouse input events. + data->glfw_active_modifiers = glfw_mods; + + // Mosis extension: Call key callback for recording + if (data->key_callback && (glfw_action == GLFW_PRESS || glfw_action == GLFW_RELEASE)) { + data->key_callback(glfw_key, glfw_action == GLFW_PRESS); + } + + // Override the default key event callback to add global shortcuts for the samples. + Rml::Context* context = data->context; + KeyDownCallback key_down_callback = data->key_down_callback; + + switch (glfw_action) + { + case GLFW_PRESS: + case GLFW_REPEAT: + { + const Rml::Input::KeyIdentifier key = RmlGLFW::ConvertKey(glfw_key); + const int key_modifier = RmlGLFW::ConvertKeyModifiers(glfw_mods); + float dp_ratio = 1.f; + glfwGetWindowContentScale(data->window, &dp_ratio, nullptr); + + // See if we have any global shortcuts that take priority over the context. + if (key_down_callback && !key_down_callback(context, key, key_modifier, dp_ratio, true)) + break; + // Otherwise, hand the event over to the context by calling the input handler as normal. + if (!RmlGLFW::ProcessKeyCallback(context, glfw_key, glfw_action, glfw_mods)) + break; + // The key was not consumed by the context either, try keyboard shortcuts of lower priority. + if (key_down_callback && !key_down_callback(context, key, key_modifier, dp_ratio, false)) + break; + } + break; + case GLFW_RELEASE: RmlGLFW::ProcessKeyCallback(context, glfw_key, glfw_action, glfw_mods); break; + } + }); + + glfwSetCharCallback(window, [](GLFWwindow* /*window*/, unsigned int codepoint) { RmlGLFW::ProcessCharCallback(data->context, codepoint); }); + + glfwSetCursorEnterCallback(window, [](GLFWwindow* /*window*/, int entered) { RmlGLFW::ProcessCursorEnterCallback(data->context, entered); }); + + // Mouse input + glfwSetCursorPosCallback(window, [](GLFWwindow* window, double xpos, double ypos) { + // Convert to framebuffer coordinates and store for recording + int fb_x, fb_y; + ConvertToFramebufferCoords(window, xpos, ypos, fb_x, fb_y); + data->mouse_x = fb_x; + data->mouse_y = fb_y; + + // Mosis extension: Call mouse move callback for recording + if (data->mouse_move_callback) { + data->mouse_move_callback(fb_x, fb_y); + } + + RmlGLFW::ProcessCursorPosCallback(data->context, window, xpos, ypos, data->glfw_active_modifiers); + }); + + glfwSetMouseButtonCallback(window, [](GLFWwindow* /*window*/, int button, int action, int mods) { + data->glfw_active_modifiers = mods; + + // Mosis extension: Call mouse button callback for recording + if (data->mouse_button_callback) { + data->mouse_button_callback(data->mouse_x, data->mouse_y, button, action == GLFW_PRESS); + } + + RmlGLFW::ProcessMouseButtonCallback(data->context, button, action, mods); + }); + + glfwSetScrollCallback(window, [](GLFWwindow* /*window*/, double /*xoffset*/, double yoffset) { + RmlGLFW::ProcessScrollCallback(data->context, yoffset, data->glfw_active_modifiers); + }); + + // Window events + glfwSetFramebufferSizeCallback(window, [](GLFWwindow* /*window*/, int width, int height) { + data->render_interface.SetViewport(width, height); + RmlGLFW::ProcessFramebufferSizeCallback(data->context, width, height); + }); + + glfwSetWindowContentScaleCallback(window, + [](GLFWwindow* /*window*/, float xscale, float /*yscale*/) { RmlGLFW::ProcessContentScaleCallback(data->context, xscale); }); +} diff --git a/designer/src/backend/RmlUi_Platform_GLFW.cpp b/designer/src/backend/RmlUi_Platform_GLFW.cpp new file mode 100644 index 0000000..e0e469b --- /dev/null +++ b/designer/src/backend/RmlUi_Platform_GLFW.cpp @@ -0,0 +1,348 @@ +/* + * This source file is part of RmlUi, the HTML/CSS Interface Middleware + * + * For the latest information, see http://github.com/mikke89/RmlUi + * + * Copyright (c) 2008-2010 CodePoint Ltd, Shift Technology Ltd + * Copyright (c) 2019-2023 The RmlUi Team, and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + */ + +#include "RmlUi_Platform_GLFW.h" +#include +#include +#include +#include +#include + +#define GLFW_HAS_EXTRA_CURSORS (GLFW_VERSION_MAJOR >= 3 && GLFW_VERSION_MINOR >= 4) + +SystemInterface_GLFW::SystemInterface_GLFW() +{ + cursor_pointer = glfwCreateStandardCursor(GLFW_HAND_CURSOR); + cursor_cross = glfwCreateStandardCursor(GLFW_CROSSHAIR_CURSOR); + cursor_text = glfwCreateStandardCursor(GLFW_IBEAM_CURSOR); +#if GLFW_HAS_EXTRA_CURSORS + cursor_move = glfwCreateStandardCursor(GLFW_RESIZE_ALL_CURSOR); + cursor_resize = glfwCreateStandardCursor(GLFW_RESIZE_NWSE_CURSOR); + cursor_unavailable = glfwCreateStandardCursor(GLFW_NOT_ALLOWED_CURSOR); +#else + cursor_move = cursor_pointer; + cursor_resize = cursor_pointer; + cursor_unavailable = nullptr; +#endif +} + +SystemInterface_GLFW::~SystemInterface_GLFW() +{ + glfwDestroyCursor(cursor_pointer); + glfwDestroyCursor(cursor_cross); + glfwDestroyCursor(cursor_text); +#if GLFW_HAS_EXTRA_CURSORS + glfwDestroyCursor(cursor_move); + glfwDestroyCursor(cursor_resize); + glfwDestroyCursor(cursor_unavailable); +#endif +} + +void SystemInterface_GLFW::SetWindow(GLFWwindow* in_window) +{ + window = in_window; +} + +double SystemInterface_GLFW::GetElapsedTime() +{ + return glfwGetTime(); +} + +void SystemInterface_GLFW::SetMouseCursor(const Rml::String& cursor_name) +{ + GLFWcursor* cursor = nullptr; + + if (cursor_name.empty() || cursor_name == "arrow") + cursor = nullptr; + else if (cursor_name == "move") + cursor = cursor_move; + else if (cursor_name == "pointer") + cursor = cursor_pointer; + else if (cursor_name == "resize") + cursor = cursor_resize; + else if (cursor_name == "cross") + cursor = cursor_cross; + else if (cursor_name == "text") + cursor = cursor_text; + else if (cursor_name == "unavailable") + cursor = cursor_unavailable; + else if (Rml::StringUtilities::StartsWith(cursor_name, "rmlui-scroll")) + cursor = cursor_move; + + if (window) + glfwSetCursor(window, cursor); +} + +void SystemInterface_GLFW::SetClipboardText(const Rml::String& text_utf8) +{ + if (window) + glfwSetClipboardString(window, text_utf8.c_str()); +} + +void SystemInterface_GLFW::GetClipboardText(Rml::String& text) +{ + if (window) + text = Rml::String(glfwGetClipboardString(window)); +} + +bool RmlGLFW::ProcessKeyCallback(Rml::Context* context, int key, int action, int mods) +{ + if (!context) + return true; + + bool result = true; + + switch (action) + { + case GLFW_PRESS: + case GLFW_REPEAT: + result = context->ProcessKeyDown(RmlGLFW::ConvertKey(key), RmlGLFW::ConvertKeyModifiers(mods)); + if (key == GLFW_KEY_ENTER || key == GLFW_KEY_KP_ENTER) + result &= context->ProcessTextInput('\n'); + break; + case GLFW_RELEASE: result = context->ProcessKeyUp(RmlGLFW::ConvertKey(key), RmlGLFW::ConvertKeyModifiers(mods)); break; + } + + return result; +} +bool RmlGLFW::ProcessCharCallback(Rml::Context* context, unsigned int codepoint) +{ + if (!context) + return true; + + bool result = context->ProcessTextInput((Rml::Character)codepoint); + return result; +} + +bool RmlGLFW::ProcessCursorEnterCallback(Rml::Context* context, int entered) +{ + if (!context) + return true; + + bool result = true; + if (!entered) + result = context->ProcessMouseLeave(); + return result; +} + +bool RmlGLFW::ProcessCursorPosCallback(Rml::Context* context, GLFWwindow* window, double xpos, double ypos, int mods) +{ + if (!context) + return true; + + using Rml::Vector2i; + using Vector2d = Rml::Vector2; + + Vector2i window_size, framebuffer_size; + glfwGetWindowSize(window, &window_size.x, &window_size.y); + glfwGetFramebufferSize(window, &framebuffer_size.x, &framebuffer_size.y); + + // Convert from mouse position in GLFW screen coordinates to framebuffer coordinates (pixels) used by RmlUi. + const Vector2d mouse_pos = Vector2d(xpos, ypos) * (Vector2d(framebuffer_size) / Vector2d(window_size)); + const Vector2i mouse_pos_round = {int(Rml::Math::Round(mouse_pos.x)), int(Rml::Math::Round(mouse_pos.y))}; + + bool result = context->ProcessMouseMove(mouse_pos_round.x, mouse_pos_round.y, RmlGLFW::ConvertKeyModifiers(mods)); + return result; +} + +bool RmlGLFW::ProcessMouseButtonCallback(Rml::Context* context, int button, int action, int mods) +{ + if (!context) + return true; + + bool result = true; + + switch (action) + { + case GLFW_PRESS: result = context->ProcessMouseButtonDown(button, RmlGLFW::ConvertKeyModifiers(mods)); break; + case GLFW_RELEASE: result = context->ProcessMouseButtonUp(button, RmlGLFW::ConvertKeyModifiers(mods)); break; + } + return result; +} + +bool RmlGLFW::ProcessScrollCallback(Rml::Context* context, double yoffset, int mods) +{ + if (!context) + return true; + + bool result = context->ProcessMouseWheel(-float(yoffset), RmlGLFW::ConvertKeyModifiers(mods)); + return result; +} + +void RmlGLFW::ProcessFramebufferSizeCallback(Rml::Context* context, int width, int height) +{ + if (context) + context->SetDimensions(Rml::Vector2i(width, height)); +} + +void RmlGLFW::ProcessContentScaleCallback(Rml::Context* context, float xscale) +{ + if (context) + context->SetDensityIndependentPixelRatio(xscale); +} + +int RmlGLFW::ConvertKeyModifiers(int glfw_mods) +{ + int key_modifier_state = 0; + + if (GLFW_MOD_SHIFT & glfw_mods) + key_modifier_state |= Rml::Input::KM_SHIFT; + + if (GLFW_MOD_CONTROL & glfw_mods) + key_modifier_state |= Rml::Input::KM_CTRL; + + if (GLFW_MOD_ALT & glfw_mods) + key_modifier_state |= Rml::Input::KM_ALT; + + if (GLFW_MOD_CAPS_LOCK & glfw_mods) + key_modifier_state |= Rml::Input::KM_SCROLLLOCK; + + if (GLFW_MOD_NUM_LOCK & glfw_mods) + key_modifier_state |= Rml::Input::KM_NUMLOCK; + + return key_modifier_state; +} + +Rml::Input::KeyIdentifier RmlGLFW::ConvertKey(int glfw_key) +{ + // clang-format off + switch (glfw_key) + { + case GLFW_KEY_A: return Rml::Input::KI_A; + case GLFW_KEY_B: return Rml::Input::KI_B; + case GLFW_KEY_C: return Rml::Input::KI_C; + case GLFW_KEY_D: return Rml::Input::KI_D; + case GLFW_KEY_E: return Rml::Input::KI_E; + case GLFW_KEY_F: return Rml::Input::KI_F; + case GLFW_KEY_G: return Rml::Input::KI_G; + case GLFW_KEY_H: return Rml::Input::KI_H; + case GLFW_KEY_I: return Rml::Input::KI_I; + case GLFW_KEY_J: return Rml::Input::KI_J; + case GLFW_KEY_K: return Rml::Input::KI_K; + case GLFW_KEY_L: return Rml::Input::KI_L; + case GLFW_KEY_M: return Rml::Input::KI_M; + case GLFW_KEY_N: return Rml::Input::KI_N; + case GLFW_KEY_O: return Rml::Input::KI_O; + case GLFW_KEY_P: return Rml::Input::KI_P; + case GLFW_KEY_Q: return Rml::Input::KI_Q; + case GLFW_KEY_R: return Rml::Input::KI_R; + case GLFW_KEY_S: return Rml::Input::KI_S; + case GLFW_KEY_T: return Rml::Input::KI_T; + case GLFW_KEY_U: return Rml::Input::KI_U; + case GLFW_KEY_V: return Rml::Input::KI_V; + case GLFW_KEY_W: return Rml::Input::KI_W; + case GLFW_KEY_X: return Rml::Input::KI_X; + case GLFW_KEY_Y: return Rml::Input::KI_Y; + case GLFW_KEY_Z: return Rml::Input::KI_Z; + + case GLFW_KEY_0: return Rml::Input::KI_0; + case GLFW_KEY_1: return Rml::Input::KI_1; + case GLFW_KEY_2: return Rml::Input::KI_2; + case GLFW_KEY_3: return Rml::Input::KI_3; + case GLFW_KEY_4: return Rml::Input::KI_4; + case GLFW_KEY_5: return Rml::Input::KI_5; + case GLFW_KEY_6: return Rml::Input::KI_6; + case GLFW_KEY_7: return Rml::Input::KI_7; + case GLFW_KEY_8: return Rml::Input::KI_8; + case GLFW_KEY_9: return Rml::Input::KI_9; + + case GLFW_KEY_BACKSPACE: return Rml::Input::KI_BACK; + case GLFW_KEY_TAB: return Rml::Input::KI_TAB; + + case GLFW_KEY_ENTER: return Rml::Input::KI_RETURN; + + case GLFW_KEY_PAUSE: return Rml::Input::KI_PAUSE; + case GLFW_KEY_CAPS_LOCK: return Rml::Input::KI_CAPITAL; + + case GLFW_KEY_ESCAPE: return Rml::Input::KI_ESCAPE; + + case GLFW_KEY_SPACE: return Rml::Input::KI_SPACE; + case GLFW_KEY_PAGE_UP: return Rml::Input::KI_PRIOR; + case GLFW_KEY_PAGE_DOWN: return Rml::Input::KI_NEXT; + case GLFW_KEY_END: return Rml::Input::KI_END; + case GLFW_KEY_HOME: return Rml::Input::KI_HOME; + case GLFW_KEY_LEFT: return Rml::Input::KI_LEFT; + case GLFW_KEY_UP: return Rml::Input::KI_UP; + case GLFW_KEY_RIGHT: return Rml::Input::KI_RIGHT; + case GLFW_KEY_DOWN: return Rml::Input::KI_DOWN; + case GLFW_KEY_PRINT_SCREEN: return Rml::Input::KI_SNAPSHOT; + case GLFW_KEY_INSERT: return Rml::Input::KI_INSERT; + case GLFW_KEY_DELETE: return Rml::Input::KI_DELETE; + + case GLFW_KEY_LEFT_SUPER: return Rml::Input::KI_LWIN; + case GLFW_KEY_RIGHT_SUPER: return Rml::Input::KI_RWIN; + + case GLFW_KEY_KP_0: return Rml::Input::KI_NUMPAD0; + case GLFW_KEY_KP_1: return Rml::Input::KI_NUMPAD1; + case GLFW_KEY_KP_2: return Rml::Input::KI_NUMPAD2; + case GLFW_KEY_KP_3: return Rml::Input::KI_NUMPAD3; + case GLFW_KEY_KP_4: return Rml::Input::KI_NUMPAD4; + case GLFW_KEY_KP_5: return Rml::Input::KI_NUMPAD5; + case GLFW_KEY_KP_6: return Rml::Input::KI_NUMPAD6; + case GLFW_KEY_KP_7: return Rml::Input::KI_NUMPAD7; + case GLFW_KEY_KP_8: return Rml::Input::KI_NUMPAD8; + case GLFW_KEY_KP_9: return Rml::Input::KI_NUMPAD9; + case GLFW_KEY_KP_ENTER: return Rml::Input::KI_NUMPADENTER; + case GLFW_KEY_KP_MULTIPLY: return Rml::Input::KI_MULTIPLY; + case GLFW_KEY_KP_ADD: return Rml::Input::KI_ADD; + case GLFW_KEY_KP_SUBTRACT: return Rml::Input::KI_SUBTRACT; + case GLFW_KEY_KP_DECIMAL: return Rml::Input::KI_DECIMAL; + case GLFW_KEY_KP_DIVIDE: return Rml::Input::KI_DIVIDE; + + case GLFW_KEY_F1: return Rml::Input::KI_F1; + case GLFW_KEY_F2: return Rml::Input::KI_F2; + case GLFW_KEY_F3: return Rml::Input::KI_F3; + case GLFW_KEY_F4: return Rml::Input::KI_F4; + case GLFW_KEY_F5: return Rml::Input::KI_F5; + case GLFW_KEY_F6: return Rml::Input::KI_F6; + case GLFW_KEY_F7: return Rml::Input::KI_F7; + case GLFW_KEY_F8: return Rml::Input::KI_F8; + case GLFW_KEY_F9: return Rml::Input::KI_F9; + case GLFW_KEY_F10: return Rml::Input::KI_F10; + case GLFW_KEY_F11: return Rml::Input::KI_F11; + case GLFW_KEY_F12: return Rml::Input::KI_F12; + case GLFW_KEY_F13: return Rml::Input::KI_F13; + case GLFW_KEY_F14: return Rml::Input::KI_F14; + case GLFW_KEY_F15: return Rml::Input::KI_F15; + case GLFW_KEY_F16: return Rml::Input::KI_F16; + case GLFW_KEY_F17: return Rml::Input::KI_F17; + case GLFW_KEY_F18: return Rml::Input::KI_F18; + case GLFW_KEY_F19: return Rml::Input::KI_F19; + case GLFW_KEY_F20: return Rml::Input::KI_F20; + case GLFW_KEY_F21: return Rml::Input::KI_F21; + case GLFW_KEY_F22: return Rml::Input::KI_F22; + case GLFW_KEY_F23: return Rml::Input::KI_F23; + case GLFW_KEY_F24: return Rml::Input::KI_F24; + + case GLFW_KEY_NUM_LOCK: return Rml::Input::KI_NUMLOCK; + case GLFW_KEY_SCROLL_LOCK: return Rml::Input::KI_SCROLL; + + case GLFW_KEY_LEFT_SHIFT: return Rml::Input::KI_LSHIFT; + case GLFW_KEY_LEFT_CONTROL: return Rml::Input::KI_LCONTROL; + case GLFW_KEY_RIGHT_SHIFT: return Rml::Input::KI_RSHIFT; + case GLFW_KEY_RIGHT_CONTROL: return Rml::Input::KI_RCONTROL; + case GLFW_KEY_MENU: return Rml::Input::KI_LMENU; + + case GLFW_KEY_KP_EQUAL: return Rml::Input::KI_OEM_NEC_EQUAL; + default: break; + } + // clang-format on + + return Rml::Input::KI_UNKNOWN; +} diff --git a/designer/src/backend/RmlUi_Platform_GLFW.h b/designer/src/backend/RmlUi_Platform_GLFW.h new file mode 100644 index 0000000..aac202d --- /dev/null +++ b/designer/src/backend/RmlUi_Platform_GLFW.h @@ -0,0 +1,81 @@ +/* + * This source file is part of RmlUi, the HTML/CSS Interface Middleware + * + * For the latest information, see http://github.com/mikke89/RmlUi + * + * Copyright (c) 2008-2010 CodePoint Ltd, Shift Technology Ltd + * Copyright (c) 2019-2023 The RmlUi Team, and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + */ + +#ifndef RMLUI_BACKENDS_PLATFORM_GLFW_H +#define RMLUI_BACKENDS_PLATFORM_GLFW_H + +#include +#include +#include +#include + +class SystemInterface_GLFW : public Rml::SystemInterface { +public: + SystemInterface_GLFW(); + ~SystemInterface_GLFW(); + + // Optionally, provide or change the window to be used for setting the mouse cursors and clipboard text. + void SetWindow(GLFWwindow* window); + + // -- Inherited from Rml::SystemInterface -- + + double GetElapsedTime() override; + + void SetMouseCursor(const Rml::String& cursor_name) override; + + void SetClipboardText(const Rml::String& text) override; + void GetClipboardText(Rml::String& text) override; + +private: + GLFWwindow* window = nullptr; + + GLFWcursor* cursor_pointer = nullptr; + GLFWcursor* cursor_cross = nullptr; + GLFWcursor* cursor_text = nullptr; + GLFWcursor* cursor_move = nullptr; + GLFWcursor* cursor_resize = nullptr; + GLFWcursor* cursor_unavailable = nullptr; +}; + +/** + Optional helper functions for the GLFW plaform. + */ +namespace RmlGLFW { + +// The following optional functions are intended to be called from their respective GLFW callback functions. The functions expect arguments passed +// directly from GLFW, in addition to the RmlUi context to apply the input or sizing event on. The input callbacks return true if the event is +// propagating, i.e. was not handled by the context. +bool ProcessKeyCallback(Rml::Context* context, int key, int action, int mods); +bool ProcessCharCallback(Rml::Context* context, unsigned int codepoint); +bool ProcessCursorEnterCallback(Rml::Context* context, int entered); +bool ProcessCursorPosCallback(Rml::Context* context, GLFWwindow* window, double xpos, double ypos, int mods); +bool ProcessMouseButtonCallback(Rml::Context* context, int button, int action, int mods); +bool ProcessScrollCallback(Rml::Context* context, double yoffset, int mods); +void ProcessFramebufferSizeCallback(Rml::Context* context, int width, int height); +void ProcessContentScaleCallback(Rml::Context* context, float xscale); + +// Converts the GLFW key to RmlUi key. +Rml::Input::KeyIdentifier ConvertKey(int glfw_key); + +// Converts the GLFW key modifiers to RmlUi key modifiers. +int ConvertKeyModifiers(int glfw_mods); + +} // namespace RmlGLFW + +#endif diff --git a/designer/src/main.cpp b/designer/src/main.cpp index edcdcdf..5d26cf1 100644 --- a/designer/src/main.cpp +++ b/designer/src/main.cpp @@ -10,7 +10,7 @@ #include #include #include -#include +#include "RmlUi_Backend.h" // Local backend with input recording hooks #include "platform.h" #include "file_interface.h" @@ -251,6 +251,30 @@ int main(int argc, const char* argv[]) if (!opts.record_file.empty()) { g_recorder = std::make_unique(opts.width, opts.height); g_record_file_path = opts.record_file; + + // Set up input callbacks for recording + Backend::SetMouseButtonCallback([](int x, int y, int button, bool pressed) { + if (g_recorder && g_recorder->IsRecording() && button == 0) { // Left mouse button only + if (pressed) { + g_recorder->RecordMouseDown(x, y); + } else { + g_recorder->RecordMouseUp(x, y); + } + } + }); + + Backend::SetMouseMoveCallback([](int x, int y) { + if (g_recorder && g_recorder->IsRecording()) { + g_recorder->RecordMouseMove(x, y); + } + }); + + Backend::SetKeyCallback([](int key, bool pressed) { + if (g_recorder && g_recorder->IsRecording()) { + g_recorder->RecordKey(key, pressed); + } + }); + LogMessage("Recording mode enabled. Press F5 to start recording."); }