work in progress
This commit is contained in:
5
.gitattributes
vendored
5
.gitattributes
vendored
@@ -3,10 +3,9 @@
|
||||
#
|
||||
# Linux start script should use lf
|
||||
/gradlew text eol=lf
|
||||
|
||||
# These are Windows script files and should use crlf
|
||||
*.bat text eol=crlf
|
||||
|
||||
# Binary files should be left untouched
|
||||
*.jar binary
|
||||
|
||||
*.tga filter=lfs diff=lfs merge=lfs -text
|
||||
*.ttf filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -5,4 +5,4 @@
|
||||
build
|
||||
.cxx
|
||||
.DS_Store
|
||||
|
||||
/designer/test/*test_result.txt
|
||||
|
||||
187
CLAUDE.md
187
CLAUDE.md
@@ -2,8 +2,23 @@
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
Mosis is a **virtual smartphone OS** for VR games and applications. It provides a phone-like device that users can interact with inside VR environments, with real smartphone functionality.
|
||||
|
||||
### Project Components
|
||||
|
||||
| Component | Location | Purpose |
|
||||
|-----------|----------|---------|
|
||||
| Android Service | `src/main/` | Native service running RmlUi renderer |
|
||||
| Desktop Designer | `designer/` | UI development with hot-reload |
|
||||
| Designer Tests | `designer-test/` | Automated UI testing framework |
|
||||
| UI Assets | `src/main/assets/` | Shared RML/RCSS/Lua assets |
|
||||
|
||||
## Build Commands
|
||||
|
||||
### Android (Gradle)
|
||||
|
||||
```bash
|
||||
# Build entire project
|
||||
./gradlew build
|
||||
@@ -30,6 +45,35 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
./gradlew connectedAndroidTest
|
||||
```
|
||||
|
||||
### Desktop Designer (CMake)
|
||||
|
||||
```bash
|
||||
# Configure (from designer/ folder)
|
||||
cmake -B build -DCMAKE_TOOLCHAIN_FILE=%VCPKG_ROOT%/scripts/buildsystems/vcpkg.cmake
|
||||
|
||||
# Build
|
||||
cmake --build build --config Debug
|
||||
|
||||
# Run with hot-reload
|
||||
./build/Debug/mosis-designer.exe ../src/main/assets/apps/home/home.rml
|
||||
|
||||
# Run with logging and hierarchy dump
|
||||
./build/Debug/mosis-designer.exe ../src/main/assets/apps/home/home.rml --log output.log --hierarchy hierarchy.json
|
||||
```
|
||||
|
||||
### Designer Tests (CMake)
|
||||
|
||||
```bash
|
||||
# Configure (from designer-test/ folder)
|
||||
cmake -B build -DCMAKE_TOOLCHAIN_FILE=%VCPKG_ROOT%/scripts/buildsystems/vcpkg.cmake
|
||||
|
||||
# Build
|
||||
cmake --build build --config Debug
|
||||
|
||||
# Run tests
|
||||
./build/Debug/designer-test.exe
|
||||
```
|
||||
|
||||
## Environment Requirements
|
||||
|
||||
Required environment variables:
|
||||
@@ -89,6 +133,145 @@ Kotlin NativeService → JNI → mosis-service (IMosisService)
|
||||
|
||||
## Dependencies
|
||||
|
||||
- vcpkg manages native dependencies (RmlUi)
|
||||
- vcpkg manages native dependencies (RmlUi, GLFW, Freetype, Lua, libpng, nlohmann-json)
|
||||
- CMake build system with vcpkg toolchain integration
|
||||
- Target architecture: arm64-v8a only
|
||||
- Android target architecture: arm64-v8a only
|
||||
- Desktop target: Windows x64 (MSVC)
|
||||
|
||||
## Desktop Designer
|
||||
|
||||
The desktop designer (`designer/`) provides rapid UI development with:
|
||||
|
||||
- **Hot-reload**: Automatically reloads when RML/RCSS/Lua files change
|
||||
- **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
|
||||
|
||||
### Key Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `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 |
|
||||
|
||||
### Command Line Options
|
||||
|
||||
```
|
||||
--log <path> Write logs to file
|
||||
--hierarchy <path> Dump UI hierarchy JSON each frame
|
||||
--dump Single-shot dump mode (screenshot + hierarchy)
|
||||
```
|
||||
|
||||
## Automated Testing Framework
|
||||
|
||||
The designer-test (`designer-test/`) provides automated UI testing:
|
||||
|
||||
### Test Architecture
|
||||
|
||||
1. **WindowController**: Finds designer window, sends mouse/keyboard events via Windows SendInput API
|
||||
2. **HierarchyReader**: Parses UI hierarchy JSON to find elements by ID/class
|
||||
3. **LogParser**: Monitors log file for navigation events
|
||||
4. **TestRunner**: Orchestrates test execution, reports results
|
||||
|
||||
### Writing Tests
|
||||
|
||||
```cpp
|
||||
// Find element by ID and click it
|
||||
bool ClickById(TestContext& ctx, const std::string& id) {
|
||||
ctx.hierarchy.Reload();
|
||||
auto element = ctx.hierarchy.FindById(id);
|
||||
if (!element) return false;
|
||||
|
||||
int x = element->bounds.centerX();
|
||||
int y = element->bounds.centerY();
|
||||
ScaleToPhysical(ctx, x, y); // Convert logical to physical coords
|
||||
ctx.window.SendClick(x, y);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Test example
|
||||
bool TestNavigateToDialer(TestContext& ctx) {
|
||||
GoHome(ctx);
|
||||
ctx.log.Clear();
|
||||
|
||||
if (!ClickById(ctx, "dock-phone")) return false;
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
|
||||
ctx.log.Reload();
|
||||
return ctx.log.Contains("Loaded screen: apps/dialer/dialer.rml");
|
||||
}
|
||||
```
|
||||
|
||||
### Test Output
|
||||
|
||||
Tests produce JSON results at `test_results.json`:
|
||||
```json
|
||||
{
|
||||
"name": "Mosis Designer UI Tests",
|
||||
"summary": {"passed": 5, "failed": 0, "total": 5},
|
||||
"tests": [
|
||||
{"name": "Navigate to Dialer", "status": "passed", "duration_ms": 3500}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## UI Assets Structure
|
||||
|
||||
All UI assets are in `src/main/assets/`:
|
||||
|
||||
```
|
||||
assets/
|
||||
├── apps/ # System apps (RML documents)
|
||||
│ ├── home/home.rml # Home screen launcher
|
||||
│ ├── dialer/dialer.rml # Phone dialer
|
||||
│ ├── messages/ # Messages app
|
||||
│ ├── contacts/ # Contacts app
|
||||
│ ├── settings/ # Settings app
|
||||
│ └── browser/ # Web browser
|
||||
├── ui/ # Shared stylesheets
|
||||
│ ├── html.rcss # Base HTML element styles
|
||||
│ ├── theme.rcss # Design tokens (colors, typography)
|
||||
│ └── components.rcss # Reusable UI components
|
||||
├── scripts/ # Lua scripts
|
||||
│ └── navigation.lua # Navigation system
|
||||
├── icons/ # TGA icon files (24x24, 32x32)
|
||||
└── fonts/ # TTF fonts (LatoLatin, Roboto)
|
||||
```
|
||||
|
||||
### Navigation System
|
||||
|
||||
Navigation is handled by `scripts/navigation.lua`:
|
||||
|
||||
```lua
|
||||
-- Navigate to a screen
|
||||
navigateTo('dialer') -- Push to history, animate forward
|
||||
|
||||
-- Go back
|
||||
goBack() -- Pop from history, animate back
|
||||
|
||||
-- Go home
|
||||
goHome() -- Clear history, return to home
|
||||
```
|
||||
|
||||
### Element IDs for Testing
|
||||
|
||||
Key elements have IDs for automated testing:
|
||||
|
||||
| ID | Location | Purpose |
|
||||
|----|----------|---------|
|
||||
| `dock-phone` | home.rml | Phone dock icon |
|
||||
| `dock-messages` | home.rml | Messages dock icon |
|
||||
| `dock-contacts` | home.rml | Contacts dock icon |
|
||||
| `dock-browser` | home.rml | Browser dock icon |
|
||||
| `app-settings` | home.rml | Settings app icon |
|
||||
|
||||
### CSS Classes for Navigation
|
||||
|
||||
Back buttons use `app-bar-nav` class for automated GoHome:
|
||||
```html
|
||||
<div class="app-bar-nav btn-icon" onclick="goBack()">
|
||||
<img src="../../icons/back.tga"/>
|
||||
</div>
|
||||
```
|
||||
|
||||
397
TESTING.md
Normal file
397
TESTING.md
Normal file
@@ -0,0 +1,397 @@
|
||||
# Mosis UI Testing Framework
|
||||
|
||||
This document describes the automated UI testing framework for Mosis virtual smartphone.
|
||||
|
||||
## Overview
|
||||
|
||||
The testing framework enables automated validation of UI behavior through:
|
||||
|
||||
1. **UI Hierarchy Inspection**: JSON dump of all UI elements with bounds
|
||||
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
|
||||
|
||||
## Components
|
||||
|
||||
### Designer (mosis-designer.exe)
|
||||
|
||||
The desktop designer serves as the test target. When launched with testing options:
|
||||
|
||||
```bash
|
||||
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
|
||||
|
||||
### Test Runner (designer-test.exe)
|
||||
|
||||
Automated test executor that:
|
||||
|
||||
1. Launches designer with testing options
|
||||
2. Waits for window to appear
|
||||
3. Reads UI hierarchy to find elements
|
||||
4. Sends input events to simulate user interaction
|
||||
5. Verifies results via log file
|
||||
6. Outputs JSON test results
|
||||
|
||||
## UI Hierarchy Format
|
||||
|
||||
The hierarchy JSON contains all visible UI elements:
|
||||
|
||||
```json
|
||||
{
|
||||
"timestamp": 1705312200,
|
||||
"screen": "apps/home/home.rml",
|
||||
"resolution": {"width": 677, "height": 1202},
|
||||
"elements": {
|
||||
"tag": "body",
|
||||
"id": "",
|
||||
"classes": ["home-screen"],
|
||||
"bounds": {"x": 0, "y": 0, "width": 677, "height": 1202},
|
||||
"visible": true,
|
||||
"text": null,
|
||||
"children": [
|
||||
{
|
||||
"tag": "div",
|
||||
"id": "dock-phone",
|
||||
"classes": ["dock-item"],
|
||||
"bounds": {"x": 85, "y": 1138, "width": 56, "height": 56},
|
||||
"visible": true,
|
||||
"text": null,
|
||||
"children": [...]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Key Fields
|
||||
|
||||
| Field | Description |
|
||||
|-------|-------------|
|
||||
| `tag` | HTML element tag (div, span, img, input) |
|
||||
| `id` | Element ID attribute (empty if none) |
|
||||
| `classes` | Array of CSS class names |
|
||||
| `bounds` | Position and size in logical pixels |
|
||||
| `visible` | Whether element is displayed |
|
||||
| `text` | Text content (for leaf text nodes) |
|
||||
| `children` | Nested child elements |
|
||||
|
||||
### Resolution Note
|
||||
|
||||
The `resolution` field contains RmlUi's logical dimensions, which may differ from physical window size due to DPI scaling. Tests must scale coordinates:
|
||||
|
||||
```cpp
|
||||
// Scale from hierarchy (logical) to window (physical)
|
||||
int physicalX = logicalX * windowWidth / hierarchyWidth;
|
||||
int physicalY = logicalY * windowHeight / hierarchyHeight;
|
||||
```
|
||||
|
||||
## Writing Tests
|
||||
|
||||
### Test Function Signature
|
||||
|
||||
```cpp
|
||||
bool MyTest(TestContext& ctx);
|
||||
```
|
||||
|
||||
TestContext provides:
|
||||
- `ctx.window` - WindowController for input
|
||||
- `ctx.log` - LogParser for log verification
|
||||
- `ctx.hierarchy` - HierarchyReader for element lookup
|
||||
|
||||
### Finding Elements
|
||||
|
||||
```cpp
|
||||
// By ID
|
||||
auto element = ctx.hierarchy.FindById("dock-phone");
|
||||
|
||||
// By class (returns all matches)
|
||||
auto buttons = ctx.hierarchy.FindByClass("btn-icon");
|
||||
|
||||
// By tag
|
||||
auto divs = ctx.hierarchy.FindByTag("div");
|
||||
```
|
||||
|
||||
### Clicking Elements
|
||||
|
||||
```cpp
|
||||
bool ClickById(TestContext& ctx, const std::string& id) {
|
||||
ctx.hierarchy.Reload(); // Get fresh hierarchy
|
||||
|
||||
auto element = ctx.hierarchy.FindById(id);
|
||||
if (!element || !element->visible) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get center point
|
||||
int x = element->bounds.centerX();
|
||||
int y = element->bounds.centerY();
|
||||
|
||||
// Scale to physical coordinates
|
||||
ScaleToPhysical(ctx, x, y);
|
||||
|
||||
// Send click
|
||||
ctx.window.SendClick(x, y);
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
### Verifying Navigation
|
||||
|
||||
```cpp
|
||||
bool TestNavigateToDialer(TestContext& ctx) {
|
||||
GoHome(ctx); // Return to home screen
|
||||
ctx.log.Clear(); // Clear previous logs
|
||||
|
||||
if (!ClickById(ctx, "dock-phone")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Wait for navigation animation
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
|
||||
|
||||
// Check log for navigation event
|
||||
ctx.log.Reload();
|
||||
return ctx.log.Contains("Loaded screen: apps/dialer/dialer.rml");
|
||||
}
|
||||
```
|
||||
|
||||
### Navigation Helpers
|
||||
|
||||
```cpp
|
||||
// Go back to home screen by clicking back buttons
|
||||
void GoHome(TestContext& ctx) {
|
||||
for (int i = 0; i < 5; ++i) {
|
||||
ctx.hierarchy.Reload();
|
||||
auto elements = ctx.hierarchy.FindByClass("app-bar-nav");
|
||||
if (!elements.empty() && elements[0].visible) {
|
||||
int x = elements[0].bounds.centerX();
|
||||
int y = elements[0].bounds.centerY();
|
||||
ScaleToPhysical(ctx, x, y);
|
||||
ctx.window.SendClick(x, y);
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(400));
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(800));
|
||||
}
|
||||
```
|
||||
|
||||
## Registering Tests
|
||||
|
||||
In `main.cpp`:
|
||||
|
||||
```cpp
|
||||
TestRunner runner;
|
||||
runner.SetDesignerPath(designerPath);
|
||||
runner.SetDocumentPath(documentPath);
|
||||
runner.SetLogPath(logPath);
|
||||
runner.SetHierarchyPath(hierarchyPath);
|
||||
|
||||
// Register tests
|
||||
runner.AddTest("Navigate to Dialer", TestNavigateToDialer);
|
||||
runner.AddTest("Navigate to Messages", TestNavigateToMessages);
|
||||
runner.AddTest("Navigate to Contacts", TestNavigateToContacts);
|
||||
runner.AddTest("Navigate to Browser", TestNavigateToBrowser);
|
||||
runner.AddTest("Navigation Sequence", TestNavigationSequence);
|
||||
|
||||
// Run all tests
|
||||
auto results = runner.RunAll();
|
||||
results.SaveToFile(resultsPath);
|
||||
```
|
||||
|
||||
## Test Results Format
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Mosis Designer UI Tests",
|
||||
"summary": {
|
||||
"passed": 5,
|
||||
"failed": 0,
|
||||
"skipped": 0,
|
||||
"errors": 0,
|
||||
"total": 5,
|
||||
"duration_ms": 31162.3
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"name": "Navigate to Dialer",
|
||||
"status": "passed",
|
||||
"message": "Test passed",
|
||||
"duration_ms": 4216.88
|
||||
},
|
||||
{
|
||||
"name": "Navigate to Browser",
|
||||
"status": "passed",
|
||||
"message": "Test passed",
|
||||
"duration_ms": 4067.34
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Running Tests
|
||||
|
||||
### Command Line
|
||||
|
||||
```bash
|
||||
# Run with defaults (auto-detects paths)
|
||||
designer-test.exe
|
||||
|
||||
# Override paths
|
||||
designer-test.exe --designer /path/to/mosis-designer.exe \
|
||||
--document /path/to/home.rml \
|
||||
--log /path/to/output.log \
|
||||
--hierarchy /path/to/hierarchy.json \
|
||||
--results /path/to/results.json
|
||||
```
|
||||
|
||||
### Exit Codes
|
||||
|
||||
- `0`: All tests passed
|
||||
- `1`: One or more tests failed
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Element Identification
|
||||
|
||||
1. **Use IDs** for clickable elements that tests need to find
|
||||
2. **Use semantic classes** like `app-bar-nav` for navigation buttons
|
||||
3. **Avoid relying on position** - use hierarchy lookup instead
|
||||
|
||||
### Timing
|
||||
|
||||
1. **Wait after navigation** (1000ms) for animations to complete
|
||||
2. **Reload hierarchy** before each element lookup
|
||||
3. **Use retry logic** for file reads (hierarchy may be mid-write)
|
||||
|
||||
### Test Independence
|
||||
|
||||
1. **Start from known state** - call GoHome() at start of each test
|
||||
2. **Clear logs** before verification
|
||||
3. **Don't depend on previous test state**
|
||||
|
||||
## Adding Test IDs to RML
|
||||
|
||||
When creating new screens, add IDs to interactive elements:
|
||||
|
||||
```html
|
||||
<!-- Good: Has ID for testing -->
|
||||
<div id="dock-phone" class="dock-item" onclick="navigateTo('dialer')">
|
||||
<img src="../../icons/phone.tga"/>
|
||||
</div>
|
||||
|
||||
<!-- Good: Has class for back button detection -->
|
||||
<div class="app-bar-nav btn-icon" onclick="goBack()">
|
||||
<img src="../../icons/back.tga"/>
|
||||
</div>
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Element Not Found
|
||||
|
||||
- Verify the screen has loaded (check hierarchy `screen` field)
|
||||
- Check if element has the expected ID/class
|
||||
- Ensure element is visible (`visible: true`)
|
||||
|
||||
### Click Not Registering
|
||||
|
||||
- Check coordinate scaling (hierarchy vs window size)
|
||||
- Increase wait time for animations
|
||||
- Verify window is in foreground
|
||||
|
||||
### Timing Issues
|
||||
|
||||
- Increase delays after GoHome() and navigation
|
||||
- Add retries for hierarchy reload
|
||||
- Check for race conditions in file I/O
|
||||
|
||||
## Android Testing
|
||||
|
||||
The Android app supports event injection for automated testing.
|
||||
|
||||
### Event Injection Methods
|
||||
|
||||
The `MainActivity` provides these methods for programmatic testing:
|
||||
|
||||
```kotlin
|
||||
// Inject touch events with normalized coordinates (0.0-1.0)
|
||||
MainActivity.instance?.injectTouchDown(x, y)
|
||||
MainActivity.instance?.injectTouchMove(x, y)
|
||||
MainActivity.instance?.injectTouchUp(x, y)
|
||||
MainActivity.instance?.injectClick(x, y) // Down + delay + Up
|
||||
```
|
||||
|
||||
### Broadcast-Based Injection
|
||||
|
||||
For external test frameworks or ADB, send broadcasts:
|
||||
|
||||
```bash
|
||||
# Inject a click at center (0.5, 0.5)
|
||||
adb shell am broadcast -a com.omixlab.mosis.INJECT_TOUCH \
|
||||
--es touch_type "click" \
|
||||
--ef x 0.5 \
|
||||
--ef y 0.5
|
||||
|
||||
# Inject touch down
|
||||
adb shell am broadcast -a com.omixlab.mosis.INJECT_TOUCH \
|
||||
--es touch_type "down" \
|
||||
--ef x 0.2 \
|
||||
--ef y 0.9
|
||||
|
||||
# Inject touch up
|
||||
adb shell am broadcast -a com.omixlab.mosis.INJECT_TOUCH \
|
||||
--es touch_type "up" \
|
||||
--ef x 0.2 \
|
||||
--ef y 0.9
|
||||
```
|
||||
|
||||
### UI Element Coordinates
|
||||
|
||||
Dock items are positioned at the bottom of the screen. Approximate normalized positions:
|
||||
|
||||
| Element | X | Y |
|
||||
|---------|---|---|
|
||||
| dock-phone | 0.16 | 0.97 |
|
||||
| dock-messages | 0.39 | 0.97 |
|
||||
| dock-contacts | 0.61 | 0.97 |
|
||||
| dock-browser | 0.84 | 0.97 |
|
||||
|
||||
### Instrumentation Tests
|
||||
|
||||
For full integration tests, use Android's instrumentation framework:
|
||||
|
||||
```kotlin
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class NavigationTest {
|
||||
@get:Rule
|
||||
val activityRule = ActivityScenarioRule(MainActivity::class.java)
|
||||
|
||||
@Test
|
||||
fun testNavigateToDialer() {
|
||||
// Wait for service to connect
|
||||
Thread.sleep(2000)
|
||||
|
||||
// Click on phone dock icon (normalized coords)
|
||||
activityRule.scenario.onActivity { activity ->
|
||||
activity.injectClick(0.16f, 0.97f)
|
||||
}
|
||||
|
||||
// Wait for navigation
|
||||
Thread.sleep(1000)
|
||||
|
||||
// Verify navigation occurred (check logs or UI state)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Future Improvements
|
||||
|
||||
- [ ] Action recording (capture user interactions)
|
||||
- [ ] Screenshot comparison (visual regression testing)
|
||||
- [ ] Android instrumentation test suite
|
||||
- [ ] Parallel test execution
|
||||
- [ ] Test coverage reporting
|
||||
- [ ] Cross-platform test runner (desktop + Android)
|
||||
34
designer-test/CMakeLists.txt
Normal file
34
designer-test/CMakeLists.txt
Normal file
@@ -0,0 +1,34 @@
|
||||
cmake_minimum_required(VERSION 3.22.1)
|
||||
project(designer-test)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 23)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
# Windows-only project
|
||||
if(NOT WIN32)
|
||||
message(FATAL_ERROR "designer-test is Windows-only")
|
||||
endif()
|
||||
|
||||
# Find nlohmann_json for test result output
|
||||
find_package(nlohmann_json CONFIG REQUIRED)
|
||||
|
||||
# Main test executable
|
||||
add_executable(designer-test
|
||||
src/main.cpp
|
||||
src/window_controller.cpp
|
||||
src/test_runner.cpp
|
||||
src/log_parser.cpp
|
||||
src/hierarchy_reader.cpp
|
||||
)
|
||||
|
||||
target_include_directories(designer-test PRIVATE src)
|
||||
|
||||
target_link_libraries(designer-test PRIVATE
|
||||
nlohmann_json::nlohmann_json
|
||||
)
|
||||
|
||||
# Link Windows libraries
|
||||
target_link_libraries(designer-test PRIVATE
|
||||
user32
|
||||
shell32
|
||||
)
|
||||
207
designer-test/src/hierarchy_reader.cpp
Normal file
207
designer-test/src/hierarchy_reader.cpp
Normal file
@@ -0,0 +1,207 @@
|
||||
// Hierarchy reader implementation
|
||||
#include "hierarchy_reader.h"
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <thread>
|
||||
#include <chrono>
|
||||
|
||||
namespace mosis::test {
|
||||
|
||||
void HierarchyReader::SetHierarchyPath(const std::filesystem::path& path) {
|
||||
m_path = path;
|
||||
m_loaded = false;
|
||||
}
|
||||
|
||||
bool HierarchyReader::Reload() {
|
||||
if (m_path.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Retry a few times with delay to handle file write race conditions
|
||||
for (int attempt = 0; attempt < 5; ++attempt) {
|
||||
if (attempt > 0) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||
}
|
||||
|
||||
std::ifstream file(m_path);
|
||||
if (!file.is_open()) {
|
||||
continue; // File might not exist yet, retry
|
||||
}
|
||||
|
||||
try {
|
||||
m_json = nlohmann::json::parse(file);
|
||||
m_loaded = true;
|
||||
return true;
|
||||
} catch (const nlohmann::json::parse_error& e) {
|
||||
// File might be partially written, retry
|
||||
if (attempt == 4) {
|
||||
std::cerr << "Failed to parse hierarchy JSON after 5 attempts: " << e.what() << std::endl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::cerr << "Failed to load hierarchy file: " << m_path << std::endl;
|
||||
m_loaded = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string HierarchyReader::GetScreen() const {
|
||||
if (!m_loaded) return "";
|
||||
return m_json.value("screen", "");
|
||||
}
|
||||
|
||||
std::string HierarchyReader::GetScreenName() const {
|
||||
std::string screen = GetScreen();
|
||||
// Extract just the filename from the path
|
||||
size_t lastSlash = screen.find_last_of("/\\");
|
||||
if (lastSlash != std::string::npos) {
|
||||
screen = screen.substr(lastSlash + 1);
|
||||
}
|
||||
return screen;
|
||||
}
|
||||
|
||||
std::vector<ElementInfo> HierarchyReader::GetAllElements() const {
|
||||
std::vector<ElementInfo> results;
|
||||
if (!m_loaded || !m_json.contains("elements")) {
|
||||
return results;
|
||||
}
|
||||
|
||||
SearchElements(m_json["elements"], [](const ElementInfo&) {
|
||||
return true; // Match all elements
|
||||
}, results);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
int HierarchyReader::GetWidth() const {
|
||||
if (!m_loaded) return 0;
|
||||
if (m_json.contains("resolution") && m_json["resolution"].contains("width")) {
|
||||
return m_json["resolution"]["width"].get<int>();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int HierarchyReader::GetHeight() const {
|
||||
if (!m_loaded) return 0;
|
||||
if (m_json.contains("resolution") && m_json["resolution"].contains("height")) {
|
||||
return m_json["resolution"]["height"].get<int>();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
ElementInfo HierarchyReader::ParseElement(const nlohmann::json& j) const {
|
||||
ElementInfo info;
|
||||
|
||||
info.tag = j.value("tag", "");
|
||||
info.id = j.value("id", "");
|
||||
info.visible = j.value("visible", false);
|
||||
|
||||
// Parse classes array
|
||||
if (j.contains("classes") && j["classes"].is_array()) {
|
||||
for (const auto& cls : j["classes"]) {
|
||||
if (cls.is_string()) {
|
||||
info.classes.push_back(cls.get<std::string>());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse bounds
|
||||
if (j.contains("bounds") && j["bounds"].is_object()) {
|
||||
const auto& b = j["bounds"];
|
||||
info.bounds.x = b.value("x", 0.0f);
|
||||
info.bounds.y = b.value("y", 0.0f);
|
||||
info.bounds.width = b.value("width", 0.0f);
|
||||
info.bounds.height = b.value("height", 0.0f);
|
||||
}
|
||||
|
||||
// Parse text
|
||||
if (j.contains("text") && !j["text"].is_null()) {
|
||||
info.text = j["text"].get<std::string>();
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
void HierarchyReader::SearchElements(
|
||||
const nlohmann::json& j,
|
||||
std::function<bool(const ElementInfo&)> predicate,
|
||||
std::vector<ElementInfo>& results) const
|
||||
{
|
||||
if (!j.is_object()) return;
|
||||
|
||||
// Parse current element
|
||||
ElementInfo info = ParseElement(j);
|
||||
if (predicate(info)) {
|
||||
results.push_back(info);
|
||||
}
|
||||
|
||||
// Recurse into children
|
||||
if (j.contains("children") && j["children"].is_array()) {
|
||||
for (const auto& child : j["children"]) {
|
||||
SearchElements(child, predicate, results);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<ElementInfo> HierarchyReader::FindById(const std::string& id) const {
|
||||
if (!m_loaded || !m_json.contains("elements")) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::vector<ElementInfo> results;
|
||||
SearchElements(m_json["elements"], [&id](const ElementInfo& e) {
|
||||
return e.id == id;
|
||||
}, results);
|
||||
|
||||
if (!results.empty()) {
|
||||
return results[0];
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::vector<ElementInfo> HierarchyReader::FindByClass(const std::string& className) const {
|
||||
std::vector<ElementInfo> results;
|
||||
if (!m_loaded || !m_json.contains("elements")) {
|
||||
return results;
|
||||
}
|
||||
|
||||
SearchElements(m_json["elements"], [&className](const ElementInfo& e) {
|
||||
for (const auto& cls : e.classes) {
|
||||
if (cls == className) return true;
|
||||
}
|
||||
return false;
|
||||
}, results);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
std::vector<ElementInfo> HierarchyReader::FindByTag(const std::string& tagName) const {
|
||||
std::vector<ElementInfo> results;
|
||||
if (!m_loaded || !m_json.contains("elements")) {
|
||||
return results;
|
||||
}
|
||||
|
||||
SearchElements(m_json["elements"], [&tagName](const ElementInfo& e) {
|
||||
return e.tag == tagName;
|
||||
}, results);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
std::optional<ElementInfo> HierarchyReader::FindFirst(
|
||||
std::function<bool(const ElementInfo&)> predicate) const
|
||||
{
|
||||
if (!m_loaded || !m_json.contains("elements")) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::vector<ElementInfo> results;
|
||||
SearchElements(m_json["elements"], predicate, results);
|
||||
|
||||
if (!results.empty()) {
|
||||
return results[0];
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
} // namespace mosis::test
|
||||
91
designer-test/src/hierarchy_reader.h
Normal file
91
designer-test/src/hierarchy_reader.h
Normal file
@@ -0,0 +1,91 @@
|
||||
// Hierarchy reader for parsing UI dump JSON files
|
||||
#pragma once
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <optional>
|
||||
#include <filesystem>
|
||||
|
||||
namespace mosis::test {
|
||||
|
||||
// Element bounds from hierarchy
|
||||
struct ElementBounds {
|
||||
float x = 0;
|
||||
float y = 0;
|
||||
float width = 0;
|
||||
float height = 0;
|
||||
|
||||
// Get center point
|
||||
int centerX() const { return static_cast<int>(x + width / 2); }
|
||||
int centerY() const { return static_cast<int>(y + height / 2); }
|
||||
};
|
||||
|
||||
// Element info from hierarchy
|
||||
struct ElementInfo {
|
||||
std::string tag;
|
||||
std::string id;
|
||||
std::vector<std::string> classes;
|
||||
ElementBounds bounds;
|
||||
bool visible = false;
|
||||
std::string text;
|
||||
};
|
||||
|
||||
// Reads and parses UI hierarchy dump files
|
||||
class HierarchyReader {
|
||||
public:
|
||||
HierarchyReader() = default;
|
||||
|
||||
// Set the path to the hierarchy file
|
||||
void SetHierarchyPath(const std::filesystem::path& path);
|
||||
|
||||
// Reload the hierarchy from file
|
||||
bool Reload();
|
||||
|
||||
// Check if hierarchy is loaded
|
||||
bool IsLoaded() const { return m_loaded; }
|
||||
|
||||
// Get screen URL from hierarchy
|
||||
std::string GetScreen() const;
|
||||
|
||||
// Get screen name (filename only) from hierarchy
|
||||
std::string GetScreenName() const;
|
||||
|
||||
// Get resolution from hierarchy
|
||||
int GetWidth() const;
|
||||
int GetHeight() const;
|
||||
|
||||
// Get all elements (for debugging)
|
||||
std::vector<ElementInfo> GetAllElements() const;
|
||||
|
||||
// Find element by ID
|
||||
std::optional<ElementInfo> FindById(const std::string& id) const;
|
||||
|
||||
// Find elements by class name
|
||||
std::vector<ElementInfo> FindByClass(const std::string& className) const;
|
||||
|
||||
// Find elements by tag name
|
||||
std::vector<ElementInfo> FindByTag(const std::string& tagName) const;
|
||||
|
||||
// Find first visible element matching a predicate
|
||||
std::optional<ElementInfo> FindFirst(
|
||||
std::function<bool(const ElementInfo&)> predicate) const;
|
||||
|
||||
// Get raw JSON (for debugging)
|
||||
const nlohmann::json& GetJson() const { return m_json; }
|
||||
|
||||
private:
|
||||
// Parse element info from JSON node
|
||||
ElementInfo ParseElement(const nlohmann::json& j) const;
|
||||
|
||||
// Recursively search for elements
|
||||
void SearchElements(const nlohmann::json& j,
|
||||
std::function<bool(const ElementInfo&)> predicate,
|
||||
std::vector<ElementInfo>& results) const;
|
||||
|
||||
std::filesystem::path m_path;
|
||||
nlohmann::json m_json;
|
||||
bool m_loaded = false;
|
||||
};
|
||||
|
||||
} // namespace mosis::test
|
||||
103
designer-test/src/log_parser.cpp
Normal file
103
designer-test/src/log_parser.cpp
Normal file
@@ -0,0 +1,103 @@
|
||||
// Log parser implementation
|
||||
#include "log_parser.h"
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
|
||||
namespace mosis::test {
|
||||
|
||||
LogParser::LogParser(const std::filesystem::path& logPath)
|
||||
: m_logPath(logPath) {
|
||||
}
|
||||
|
||||
void LogParser::SetLogPath(const std::filesystem::path& path) {
|
||||
m_logPath = path;
|
||||
Clear();
|
||||
}
|
||||
|
||||
bool LogParser::Reload() {
|
||||
if (m_logPath.empty() || !std::filesystem::exists(m_logPath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::ifstream file(m_logPath);
|
||||
if (!file.is_open()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read all lines
|
||||
m_entries.clear();
|
||||
std::string line;
|
||||
while (std::getline(file, line)) {
|
||||
if (!line.empty()) {
|
||||
LogEntry entry;
|
||||
entry.message = line;
|
||||
m_entries.push_back(entry);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void LogParser::Clear() {
|
||||
m_entries.clear();
|
||||
m_lastReadPosition = 0;
|
||||
}
|
||||
|
||||
std::optional<LogEntry> LogParser::FindEntry(const std::string& pattern) const {
|
||||
// Search from newest to oldest
|
||||
for (auto it = m_entries.rbegin(); it != m_entries.rend(); ++it) {
|
||||
if (it->message.find(pattern) != std::string::npos) {
|
||||
return *it;
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
bool LogParser::Contains(const std::string& pattern) const {
|
||||
return FindEntry(pattern).has_value();
|
||||
}
|
||||
|
||||
std::vector<LogEntry> LogParser::FindAllEntries(const std::string& pattern) const {
|
||||
std::vector<LogEntry> results;
|
||||
for (const auto& entry : m_entries) {
|
||||
if (entry.message.find(pattern) != std::string::npos) {
|
||||
results.push_back(entry);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
bool LogParser::WaitForEntry(const std::string& pattern, int timeoutMs) {
|
||||
auto startTime = std::chrono::steady_clock::now();
|
||||
|
||||
while (true) {
|
||||
Reload();
|
||||
if (Contains(pattern)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
auto elapsed = std::chrono::steady_clock::now() - startTime;
|
||||
if (std::chrono::duration_cast<std::chrono::milliseconds>(elapsed).count() >= timeoutMs) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<LogEntry> LogParser::GetLastEntries(size_t count) const {
|
||||
if (count >= m_entries.size()) {
|
||||
return m_entries;
|
||||
}
|
||||
|
||||
std::vector<LogEntry> result;
|
||||
result.reserve(count);
|
||||
for (size_t i = m_entries.size() - count; i < m_entries.size(); ++i) {
|
||||
result.push_back(m_entries[i]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace mosis::test
|
||||
55
designer-test/src/log_parser.h
Normal file
55
designer-test/src/log_parser.h
Normal file
@@ -0,0 +1,55 @@
|
||||
// Log file parser for verifying navigation
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <optional>
|
||||
#include <filesystem>
|
||||
|
||||
namespace mosis::test {
|
||||
|
||||
// Represents a parsed log entry
|
||||
struct LogEntry {
|
||||
std::string message;
|
||||
std::string timestamp; // If available
|
||||
};
|
||||
|
||||
class LogParser {
|
||||
public:
|
||||
LogParser() = default;
|
||||
explicit LogParser(const std::filesystem::path& logPath);
|
||||
|
||||
// Set the log file path
|
||||
void SetLogPath(const std::filesystem::path& path);
|
||||
|
||||
// Reload log file from disk
|
||||
bool Reload();
|
||||
|
||||
// Clear cached entries (for fresh start)
|
||||
void Clear();
|
||||
|
||||
// Get all entries
|
||||
const std::vector<LogEntry>& GetEntries() const { return m_entries; }
|
||||
|
||||
// Search for a pattern in log entries (returns first match or nullopt)
|
||||
std::optional<LogEntry> FindEntry(const std::string& pattern) const;
|
||||
|
||||
// Check if pattern exists in log
|
||||
bool Contains(const std::string& pattern) const;
|
||||
|
||||
// Get entries matching a pattern
|
||||
std::vector<LogEntry> FindAllEntries(const std::string& pattern) const;
|
||||
|
||||
// Wait for a pattern to appear in the log (reloads periodically)
|
||||
bool WaitForEntry(const std::string& pattern, int timeoutMs = 5000);
|
||||
|
||||
// Get last N entries
|
||||
std::vector<LogEntry> GetLastEntries(size_t count) const;
|
||||
|
||||
private:
|
||||
std::filesystem::path m_logPath;
|
||||
std::vector<LogEntry> m_entries;
|
||||
size_t m_lastReadPosition = 0;
|
||||
};
|
||||
|
||||
} // namespace mosis::test
|
||||
364
designer-test/src/main.cpp
Normal file
364
designer-test/src/main.cpp
Normal file
@@ -0,0 +1,364 @@
|
||||
// Mosis Designer Test - Automated UI Testing
|
||||
// Sends input events directly to the designer window using element coordinates from hierarchy dump
|
||||
|
||||
#include "test_runner.h"
|
||||
#include <iostream>
|
||||
#include <filesystem>
|
||||
#include <thread>
|
||||
#include <chrono>
|
||||
|
||||
using namespace mosis::test;
|
||||
|
||||
// Helper: Scale coordinates from hierarchy (logical) to window (physical) space
|
||||
void ScaleToPhysical(TestContext& ctx, int& x, int& y) {
|
||||
// The hierarchy reports coordinates in RmlUi's logical space
|
||||
// which may be DPI-scaled. We need to convert to physical window coordinates.
|
||||
int hierarchyWidth = ctx.hierarchy.GetWidth();
|
||||
int hierarchyHeight = ctx.hierarchy.GetHeight();
|
||||
int windowWidth = ctx.window.GetInfo().clientWidth;
|
||||
int windowHeight = ctx.window.GetInfo().clientHeight;
|
||||
|
||||
if (hierarchyWidth > 0 && hierarchyHeight > 0) {
|
||||
x = static_cast<int>(x * static_cast<float>(windowWidth) / hierarchyWidth);
|
||||
y = static_cast<int>(y * static_cast<float>(windowHeight) / hierarchyHeight);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper: Click on an element found by ID in the hierarchy
|
||||
bool ClickById(TestContext& ctx, const std::string& id) {
|
||||
ctx.hierarchy.Reload();
|
||||
|
||||
// Debug: Print hierarchy info
|
||||
std::cout << " Hierarchy: " << ctx.hierarchy.GetWidth() << "x" << ctx.hierarchy.GetHeight()
|
||||
<< ", screen: " << ctx.hierarchy.GetScreenName() << std::endl;
|
||||
|
||||
auto element = ctx.hierarchy.FindById(id);
|
||||
if (!element) {
|
||||
std::cerr << " Element not found: #" << id << std::endl;
|
||||
// List available IDs for debugging
|
||||
auto allElements = ctx.hierarchy.GetAllElements();
|
||||
std::cerr << " Available IDs: ";
|
||||
for (const auto& e : allElements) {
|
||||
if (!e.id.empty()) {
|
||||
std::cerr << "#" << e.id << " ";
|
||||
}
|
||||
}
|
||||
std::cerr << std::endl;
|
||||
return false;
|
||||
}
|
||||
if (!element->visible) {
|
||||
std::cerr << " Element not visible: #" << id << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
int x = element->bounds.centerX();
|
||||
int y = element->bounds.centerY();
|
||||
int logicalX = x, logicalY = y;
|
||||
ScaleToPhysical(ctx, x, y);
|
||||
|
||||
std::cout << " Clicking #" << id << " at logical(" << logicalX << "," << logicalY
|
||||
<< ") -> physical(" << x << "," << y << ")" << std::endl;
|
||||
ctx.window.SendClick(x, y);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Helper: Click on an element found by class name (first visible match)
|
||||
bool ClickByClass(TestContext& ctx, const std::string& className) {
|
||||
ctx.hierarchy.Reload();
|
||||
auto elements = ctx.hierarchy.FindByClass(className);
|
||||
for (const auto& element : elements) {
|
||||
if (element.visible && element.bounds.width > 0 && element.bounds.height > 0) {
|
||||
int x = element.bounds.centerX();
|
||||
int y = element.bounds.centerY();
|
||||
ScaleToPhysical(ctx, x, y);
|
||||
std::cout << " Clicking ." << className << " at physical(" << x << "," << y << ")" << std::endl;
|
||||
ctx.window.SendClick(x, y);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
std::cerr << " No visible element found with class: " << className << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Helper: Click on an element found by class name at a specific index
|
||||
bool ClickByClassIndex(TestContext& ctx, const std::string& className, int index) {
|
||||
ctx.hierarchy.Reload();
|
||||
auto elements = ctx.hierarchy.FindByClass(className);
|
||||
int visibleIndex = 0;
|
||||
for (const auto& element : elements) {
|
||||
if (element.visible && element.bounds.width > 0 && element.bounds.height > 0) {
|
||||
if (visibleIndex == index) {
|
||||
int x = element.bounds.centerX();
|
||||
int y = element.bounds.centerY();
|
||||
ScaleToPhysical(ctx, x, y);
|
||||
std::cout << " Clicking ." << className << "[" << index << "] at physical("
|
||||
<< x << "," << y << ")" << std::endl;
|
||||
ctx.window.SendClick(x, y);
|
||||
return true;
|
||||
}
|
||||
visibleIndex++;
|
||||
}
|
||||
}
|
||||
std::cerr << " Element ." << className << "[" << index << "] not found (only "
|
||||
<< visibleIndex << " visible)" << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Helper: Go back to home screen by clicking back button multiple times
|
||||
void GoHome(TestContext& ctx) {
|
||||
// Click back button (app-bar-nav) up to 5 times to ensure we're at home
|
||||
for (int i = 0; i < 5; ++i) {
|
||||
ctx.hierarchy.Reload();
|
||||
// Look for the back button by class
|
||||
auto elements = ctx.hierarchy.FindByClass("app-bar-nav");
|
||||
if (!elements.empty()) {
|
||||
auto& btn = elements[0];
|
||||
if (btn.visible && btn.bounds.width > 0) {
|
||||
int x = btn.bounds.centerX();
|
||||
int y = btn.bounds.centerY();
|
||||
ScaleToPhysical(ctx, x, y);
|
||||
ctx.window.SendClick(x, y);
|
||||
}
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(400));
|
||||
}
|
||||
// Extra wait for animations to fully complete and hierarchy to update
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(800));
|
||||
}
|
||||
|
||||
// Test: Navigate to dialer by clicking Phone dock icon
|
||||
bool TestNavigateToDialer(TestContext& ctx) {
|
||||
// Ensure we start from home screen
|
||||
GoHome(ctx);
|
||||
ctx.log.Clear();
|
||||
|
||||
// Click on Phone dock icon (dock-phone id)
|
||||
if (!ClickById(ctx, "dock-phone")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Wait for navigation
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
|
||||
|
||||
// Reload log and check for navigation
|
||||
ctx.log.Reload();
|
||||
return ctx.log.Contains("Loaded screen: apps/dialer/dialer.rml") ||
|
||||
ctx.log.Contains("apps/dialer");
|
||||
}
|
||||
|
||||
// Test: Navigate to messages
|
||||
bool TestNavigateToMessages(TestContext& ctx) {
|
||||
// Ensure we start from home screen
|
||||
GoHome(ctx);
|
||||
ctx.log.Clear();
|
||||
|
||||
// Click on Messages dock icon
|
||||
if (!ClickById(ctx, "dock-messages")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
|
||||
|
||||
ctx.log.Reload();
|
||||
return ctx.log.Contains("Loaded screen: apps/messages/messages.rml");
|
||||
}
|
||||
|
||||
// Test: Navigate to contacts
|
||||
bool TestNavigateToContacts(TestContext& ctx) {
|
||||
// Ensure we start from home screen
|
||||
GoHome(ctx);
|
||||
ctx.log.Clear();
|
||||
|
||||
// Click on Contacts dock icon
|
||||
if (!ClickById(ctx, "dock-contacts")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
|
||||
|
||||
ctx.log.Reload();
|
||||
return ctx.log.Contains("Loaded screen: apps/contacts/contacts.rml");
|
||||
}
|
||||
|
||||
// Test: Navigate to browser
|
||||
bool TestNavigateToBrowser(TestContext& ctx) {
|
||||
// Ensure we start from home screen
|
||||
GoHome(ctx);
|
||||
ctx.log.Clear();
|
||||
|
||||
// Click on Browser dock icon
|
||||
if (!ClickById(ctx, "dock-browser")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
|
||||
|
||||
ctx.log.Reload();
|
||||
return ctx.log.Contains("Loaded screen: apps/browser/browser.rml");
|
||||
}
|
||||
|
||||
// Test: Navigate to settings (in app grid, not dock)
|
||||
bool TestNavigateToSettings(TestContext& ctx) {
|
||||
// Ensure we start from home screen
|
||||
GoHome(ctx);
|
||||
ctx.log.Clear();
|
||||
|
||||
// Settings is in the app grid - find by ID
|
||||
if (!ClickById(ctx, "app-settings")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
|
||||
|
||||
ctx.log.Reload();
|
||||
return ctx.log.Contains("Loaded screen: apps/settings/settings.rml");
|
||||
}
|
||||
|
||||
// Test: Multiple navigation sequence from home
|
||||
bool TestNavigationSequence(TestContext& ctx) {
|
||||
// Ensure we start from home screen
|
||||
GoHome(ctx);
|
||||
ctx.log.Clear();
|
||||
|
||||
// Go to dialer from home
|
||||
if (!ClickById(ctx, "dock-phone")) {
|
||||
std::cerr << " Failed to click dock-phone" << std::endl;
|
||||
return false;
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
|
||||
|
||||
// Go back to home
|
||||
GoHome(ctx);
|
||||
|
||||
// Go to messages from home
|
||||
if (!ClickById(ctx, "dock-messages")) {
|
||||
std::cerr << " Failed to click dock-messages" << std::endl;
|
||||
return false;
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
|
||||
|
||||
// Go back to home
|
||||
GoHome(ctx);
|
||||
|
||||
// Go to contacts from home
|
||||
if (!ClickById(ctx, "dock-contacts")) {
|
||||
std::cerr << " Failed to click dock-contacts" << std::endl;
|
||||
return false;
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
|
||||
|
||||
ctx.log.Reload();
|
||||
|
||||
// Verify all navigations happened
|
||||
bool hasDialer = ctx.log.Contains("apps/dialer");
|
||||
bool hasMessages = ctx.log.Contains("apps/messages");
|
||||
bool hasContacts = ctx.log.Contains("apps/contacts");
|
||||
|
||||
std::cout << " Dialer: " << (hasDialer ? "yes" : "no") << std::endl;
|
||||
std::cout << " Messages: " << (hasMessages ? "yes" : "no") << std::endl;
|
||||
std::cout << " Contacts: " << (hasContacts ? "yes" : "no") << std::endl;
|
||||
|
||||
return hasDialer && hasMessages && hasContacts;
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
std::cout << "Mosis Designer Test v0.2.0" << std::endl;
|
||||
std::cout << "==========================" << std::endl;
|
||||
std::cout << "Using UI hierarchy for element coordinates" << std::endl;
|
||||
|
||||
// Determine paths
|
||||
// exe is at: designer-test/build/Debug/designer-test.exe
|
||||
// project root is: MosisService/
|
||||
std::filesystem::path exePath = std::filesystem::absolute(argv[0]).parent_path();
|
||||
std::filesystem::path projectRoot = exePath.parent_path().parent_path().parent_path(); // Up 3 levels
|
||||
|
||||
std::filesystem::path designerPath = projectRoot / "designer" / "build" / "Debug" / "mosis-designer.exe";
|
||||
std::filesystem::path documentPath = projectRoot / "src" / "main" / "assets" / "apps" / "home" / "home.rml";
|
||||
std::filesystem::path logPath = exePath / "designer_test.log";
|
||||
std::filesystem::path hierarchyPath = exePath / "ui_hierarchy.json";
|
||||
std::filesystem::path resultsPath = exePath / "test_results.json";
|
||||
|
||||
// Allow command-line overrides
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
std::string arg = argv[i];
|
||||
if (arg == "--designer" && i + 1 < argc) {
|
||||
designerPath = argv[++i];
|
||||
} else if (arg == "--document" && i + 1 < argc) {
|
||||
documentPath = argv[++i];
|
||||
} else if (arg == "--log" && i + 1 < argc) {
|
||||
logPath = argv[++i];
|
||||
} else if (arg == "--hierarchy" && i + 1 < argc) {
|
||||
hierarchyPath = argv[++i];
|
||||
} else if (arg == "--results" && i + 1 < argc) {
|
||||
resultsPath = argv[++i];
|
||||
} else if (arg == "--help") {
|
||||
std::cout << "Usage: designer-test [options]" << std::endl;
|
||||
std::cout << "Options:" << std::endl;
|
||||
std::cout << " --designer PATH Path to mosis-designer.exe" << std::endl;
|
||||
std::cout << " --document PATH Path to initial RML document" << std::endl;
|
||||
std::cout << " --log PATH Path for log file" << std::endl;
|
||||
std::cout << " --hierarchy PATH Path for UI hierarchy dump file" << std::endl;
|
||||
std::cout << " --results PATH Path for JSON test results" << std::endl;
|
||||
std::cout << " --help Show this help" << std::endl;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
std::cout << "Designer: " << designerPath << std::endl;
|
||||
std::cout << "Document: " << documentPath << std::endl;
|
||||
std::cout << "Log file: " << logPath << std::endl;
|
||||
std::cout << "Hierarchy: " << hierarchyPath << std::endl;
|
||||
|
||||
// Check if designer exists
|
||||
if (!std::filesystem::exists(designerPath)) {
|
||||
std::cerr << "ERROR: Designer not found at: " << designerPath << std::endl;
|
||||
std::cerr << "Build the designer first: cd designer && cmake --build build --config Debug" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (!std::filesystem::exists(documentPath)) {
|
||||
std::cerr << "ERROR: Document not found at: " << documentPath << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Create test runner
|
||||
TestRunner runner;
|
||||
runner.SetDesignerPath(designerPath);
|
||||
runner.SetDocumentPath(documentPath);
|
||||
runner.SetLogPath(logPath);
|
||||
runner.SetHierarchyPath(hierarchyPath);
|
||||
runner.SetStartupTimeoutMs(15000);
|
||||
runner.SetActionDelayMs(500);
|
||||
|
||||
// Register tests
|
||||
runner.AddTest("Navigate to Dialer", TestNavigateToDialer);
|
||||
runner.AddTest("Navigate to Messages", TestNavigateToMessages);
|
||||
runner.AddTest("Navigate to Contacts", TestNavigateToContacts);
|
||||
runner.AddTest("Navigate to Browser", TestNavigateToBrowser);
|
||||
runner.AddTest("Navigation Sequence", TestNavigationSequence);
|
||||
|
||||
// Start designer
|
||||
std::cout << "\nStarting designer..." << std::endl;
|
||||
if (!runner.StartDesigner()) {
|
||||
std::cerr << "Failed to start designer" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::cout << "Designer started successfully" << std::endl;
|
||||
std::cout << "Window info:" << std::endl;
|
||||
const auto& info = runner.GetWindow().GetInfo();
|
||||
std::cout << " Scale: " << info.scaleX << " x " << info.scaleY << std::endl;
|
||||
|
||||
// Run tests
|
||||
auto results = runner.RunAll();
|
||||
|
||||
// Save results
|
||||
results.SaveToFile(resultsPath);
|
||||
|
||||
// Stop designer
|
||||
std::cout << "Stopping designer..." << std::endl;
|
||||
runner.StopDesigner();
|
||||
|
||||
// Return exit code based on results
|
||||
return (results.failed + results.errors) > 0 ? 1 : 0;
|
||||
}
|
||||
277
designer-test/src/test_runner.cpp
Normal file
277
designer-test/src/test_runner.cpp
Normal file
@@ -0,0 +1,277 @@
|
||||
// Test runner implementation
|
||||
#include "test_runner.h"
|
||||
#include <iostream>
|
||||
#include <fstream>
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
|
||||
namespace mosis::test {
|
||||
|
||||
nlohmann::json TestResult::ToJson() const {
|
||||
nlohmann::json j;
|
||||
j["name"] = name;
|
||||
j["status"] = status == TestStatus::Passed ? "passed" :
|
||||
status == TestStatus::Failed ? "failed" :
|
||||
status == TestStatus::Skipped ? "skipped" : "error";
|
||||
j["message"] = message;
|
||||
j["duration_ms"] = durationMs;
|
||||
return j;
|
||||
}
|
||||
|
||||
nlohmann::json TestSuiteResult::ToJson() const {
|
||||
nlohmann::json j;
|
||||
j["name"] = name;
|
||||
j["summary"] = {
|
||||
{"passed", passed},
|
||||
{"failed", failed},
|
||||
{"skipped", skipped},
|
||||
{"errors", errors},
|
||||
{"total", static_cast<int>(tests.size())},
|
||||
{"duration_ms", totalDurationMs}
|
||||
};
|
||||
|
||||
nlohmann::json testsJson = nlohmann::json::array();
|
||||
for (const auto& test : tests) {
|
||||
testsJson.push_back(test.ToJson());
|
||||
}
|
||||
j["tests"] = testsJson;
|
||||
|
||||
return j;
|
||||
}
|
||||
|
||||
void TestSuiteResult::SaveToFile(const std::filesystem::path& path) const {
|
||||
std::ofstream file(path);
|
||||
if (file.is_open()) {
|
||||
file << ToJson().dump(2);
|
||||
std::cout << "Test results saved to: " << path << std::endl;
|
||||
} else {
|
||||
std::cerr << "Failed to save test results to: " << path << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
TestRunner::TestRunner() = default;
|
||||
|
||||
TestRunner::~TestRunner() {
|
||||
StopDesigner();
|
||||
}
|
||||
|
||||
void TestRunner::SetDesignerPath(const std::filesystem::path& path) {
|
||||
m_designerPath = path;
|
||||
}
|
||||
|
||||
void TestRunner::SetDocumentPath(const std::filesystem::path& path) {
|
||||
m_documentPath = path;
|
||||
}
|
||||
|
||||
void TestRunner::SetLogPath(const std::filesystem::path& path) {
|
||||
m_logPath = path;
|
||||
m_log.SetLogPath(path);
|
||||
}
|
||||
|
||||
void TestRunner::SetHierarchyPath(const std::filesystem::path& path) {
|
||||
m_hierarchyPath = path;
|
||||
m_hierarchy.SetHierarchyPath(path);
|
||||
}
|
||||
|
||||
void TestRunner::AddTest(const std::string& name, TestFunc func) {
|
||||
m_tests.push_back({name, std::move(func)});
|
||||
}
|
||||
|
||||
bool TestRunner::StartDesigner() {
|
||||
if (m_designerPath.empty() || m_documentPath.empty()) {
|
||||
std::cerr << "Designer or document path not set" << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Delete old log file
|
||||
if (!m_logPath.empty() && std::filesystem::exists(m_logPath)) {
|
||||
std::filesystem::remove(m_logPath);
|
||||
}
|
||||
|
||||
// Delete old hierarchy file
|
||||
if (!m_hierarchyPath.empty() && std::filesystem::exists(m_hierarchyPath)) {
|
||||
std::filesystem::remove(m_hierarchyPath);
|
||||
}
|
||||
|
||||
// Build command line
|
||||
std::string cmdLine = "\"" + m_designerPath.string() + "\" \"" + m_documentPath.string() + "\"";
|
||||
if (!m_logPath.empty()) {
|
||||
cmdLine += " --log \"" + m_logPath.string() + "\"";
|
||||
}
|
||||
if (!m_hierarchyPath.empty()) {
|
||||
cmdLine += " --hierarchy \"" + m_hierarchyPath.string() + "\"";
|
||||
}
|
||||
|
||||
std::cout << "Starting designer: " << cmdLine << std::endl;
|
||||
|
||||
// Create process
|
||||
STARTUPINFOA si = {};
|
||||
si.cb = sizeof(si);
|
||||
PROCESS_INFORMATION pi = {};
|
||||
|
||||
// Need a mutable buffer for CreateProcess
|
||||
std::vector<char> cmdLineBuffer(cmdLine.begin(), cmdLine.end());
|
||||
cmdLineBuffer.push_back('\0');
|
||||
|
||||
if (!CreateProcessA(
|
||||
nullptr,
|
||||
cmdLineBuffer.data(),
|
||||
nullptr,
|
||||
nullptr,
|
||||
FALSE,
|
||||
0,
|
||||
nullptr,
|
||||
nullptr,
|
||||
&si,
|
||||
&pi)) {
|
||||
std::cerr << "Failed to start designer process: " << GetLastError() << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
m_designerProcess = pi.hProcess;
|
||||
CloseHandle(pi.hThread);
|
||||
|
||||
std::cout << "Designer process started with PID: " << pi.dwProcessId << std::endl;
|
||||
|
||||
// Wait for window to appear
|
||||
if (!m_window.WaitForWindow("Mosis Designer", m_startupTimeoutMs)) {
|
||||
std::cerr << "Designer window did not appear" << std::endl;
|
||||
StopDesigner();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Resize window to full phone resolution to ensure consistent UI layout
|
||||
m_window.ResizeToPhone();
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(500));
|
||||
|
||||
// Wait for document to load
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(2000));
|
||||
|
||||
// Verify log file was created
|
||||
if (!m_logPath.empty()) {
|
||||
m_log.Reload();
|
||||
if (!m_log.Contains("Document loaded successfully")) {
|
||||
std::cerr << "Warning: Document load confirmation not found in log" << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void TestRunner::StopDesigner() {
|
||||
if (m_designerProcess) {
|
||||
// Try graceful close first
|
||||
m_window.Close();
|
||||
|
||||
// Wait briefly for graceful exit
|
||||
if (WaitForSingleObject(m_designerProcess, 2000) == WAIT_TIMEOUT) {
|
||||
// Force terminate
|
||||
TerminateProcess(m_designerProcess, 1);
|
||||
}
|
||||
|
||||
CloseHandle(m_designerProcess);
|
||||
m_designerProcess = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
TestResult TestRunner::ExecuteTest(const TestCase& test) {
|
||||
TestResult result;
|
||||
result.name = test.name;
|
||||
|
||||
auto startTime = std::chrono::steady_clock::now();
|
||||
|
||||
try {
|
||||
// Reload hierarchy before each test
|
||||
m_hierarchy.Reload();
|
||||
|
||||
TestContext ctx{m_window, m_log, m_hierarchy};
|
||||
|
||||
// Clear log before test for cleaner verification
|
||||
m_log.Clear();
|
||||
|
||||
bool passed = test.func(ctx);
|
||||
|
||||
result.status = passed ? TestStatus::Passed : TestStatus::Failed;
|
||||
result.message = passed ? "Test passed" : "Test assertions failed";
|
||||
|
||||
} catch (const std::exception& e) {
|
||||
result.status = TestStatus::Error;
|
||||
result.message = std::string("Exception: ") + e.what();
|
||||
}
|
||||
|
||||
auto endTime = std::chrono::steady_clock::now();
|
||||
result.durationMs = std::chrono::duration<double, std::milli>(endTime - startTime).count();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
TestResult TestRunner::RunTest(const std::string& name) {
|
||||
for (const auto& test : m_tests) {
|
||||
if (test.name == name) {
|
||||
return ExecuteTest(test);
|
||||
}
|
||||
}
|
||||
|
||||
TestResult result;
|
||||
result.name = name;
|
||||
result.status = TestStatus::Error;
|
||||
result.message = "Test not found";
|
||||
return result;
|
||||
}
|
||||
|
||||
TestSuiteResult TestRunner::RunAll() {
|
||||
TestSuiteResult suite;
|
||||
suite.name = "Mosis Designer UI Tests";
|
||||
|
||||
auto suiteStart = std::chrono::steady_clock::now();
|
||||
|
||||
std::cout << "\n========================================" << std::endl;
|
||||
std::cout << "Running " << m_tests.size() << " tests" << std::endl;
|
||||
std::cout << "========================================\n" << std::endl;
|
||||
|
||||
for (const auto& test : m_tests) {
|
||||
std::cout << "Running: " << test.name << "..." << std::endl;
|
||||
|
||||
TestResult result = ExecuteTest(test);
|
||||
suite.tests.push_back(result);
|
||||
|
||||
switch (result.status) {
|
||||
case TestStatus::Passed:
|
||||
suite.passed++;
|
||||
std::cout << " PASSED (" << result.durationMs << "ms)" << std::endl;
|
||||
break;
|
||||
case TestStatus::Failed:
|
||||
suite.failed++;
|
||||
std::cout << " FAILED: " << result.message << std::endl;
|
||||
break;
|
||||
case TestStatus::Skipped:
|
||||
suite.skipped++;
|
||||
std::cout << " SKIPPED: " << result.message << std::endl;
|
||||
break;
|
||||
case TestStatus::Error:
|
||||
suite.errors++;
|
||||
std::cout << " ERROR: " << result.message << std::endl;
|
||||
break;
|
||||
}
|
||||
|
||||
// Delay between tests
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(m_actionDelayMs));
|
||||
}
|
||||
|
||||
auto suiteEnd = std::chrono::steady_clock::now();
|
||||
suite.totalDurationMs = std::chrono::duration<double, std::milli>(suiteEnd - suiteStart).count();
|
||||
|
||||
std::cout << "\n========================================" << std::endl;
|
||||
std::cout << "Test Results:" << std::endl;
|
||||
std::cout << " Passed: " << suite.passed << std::endl;
|
||||
std::cout << " Failed: " << suite.failed << std::endl;
|
||||
std::cout << " Skipped: " << suite.skipped << std::endl;
|
||||
std::cout << " Errors: " << suite.errors << std::endl;
|
||||
std::cout << " Total: " << suite.tests.size() << std::endl;
|
||||
std::cout << " Duration: " << suite.totalDurationMs << "ms" << std::endl;
|
||||
std::cout << "========================================\n" << std::endl;
|
||||
|
||||
return suite;
|
||||
}
|
||||
|
||||
} // namespace mosis::test
|
||||
149
designer-test/src/test_runner.h
Normal file
149
designer-test/src/test_runner.h
Normal file
@@ -0,0 +1,149 @@
|
||||
// Test runner for designer UI tests
|
||||
#pragma once
|
||||
|
||||
#include "window_controller.h"
|
||||
#include "log_parser.h"
|
||||
#include "hierarchy_reader.h"
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <functional>
|
||||
#include <filesystem>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
namespace mosis::test {
|
||||
|
||||
// Test result status
|
||||
enum class TestStatus {
|
||||
Passed,
|
||||
Failed,
|
||||
Skipped,
|
||||
Error
|
||||
};
|
||||
|
||||
// Individual test result
|
||||
struct TestResult {
|
||||
std::string name;
|
||||
TestStatus status;
|
||||
std::string message;
|
||||
double durationMs;
|
||||
|
||||
nlohmann::json ToJson() const;
|
||||
};
|
||||
|
||||
// Test suite result
|
||||
struct TestSuiteResult {
|
||||
std::string name;
|
||||
std::vector<TestResult> tests;
|
||||
int passed = 0;
|
||||
int failed = 0;
|
||||
int skipped = 0;
|
||||
int errors = 0;
|
||||
double totalDurationMs = 0;
|
||||
|
||||
nlohmann::json ToJson() const;
|
||||
void SaveToFile(const std::filesystem::path& path) const;
|
||||
};
|
||||
|
||||
// Test context passed to each test
|
||||
struct TestContext {
|
||||
WindowController& window;
|
||||
LogParser& log;
|
||||
HierarchyReader& hierarchy;
|
||||
};
|
||||
|
||||
// Test function signature
|
||||
using TestFunc = std::function<bool(TestContext&)>;
|
||||
|
||||
// Individual test case
|
||||
struct TestCase {
|
||||
std::string name;
|
||||
TestFunc func;
|
||||
};
|
||||
|
||||
// App icon positions for tests
|
||||
struct AppPosition {
|
||||
int x;
|
||||
int y;
|
||||
};
|
||||
|
||||
// Predefined positions on phone screen (540x960)
|
||||
namespace AppPositions {
|
||||
// Navigation buttons (in app-bar, 56px high starting after 24px status bar)
|
||||
// Status bar: y=0-24, App bar: y=24-80, back button at left with 16px padding
|
||||
// Use y=40 which is in app-bar on screens with app-bar, but in padding on home
|
||||
constexpr AppPosition BackButton = {40, 40}; // In app-bar zone, safe on home screen
|
||||
|
||||
// App grid positions - Row 1 (starting around y=100 after app-bar)
|
||||
constexpr AppPosition Phone = {67, 120};
|
||||
constexpr AppPosition Messages = {202, 120};
|
||||
constexpr AppPosition Contacts = {337, 120};
|
||||
constexpr AppPosition Browser = {472, 120};
|
||||
// Row 2
|
||||
constexpr AppPosition Gallery = {67, 220};
|
||||
constexpr AppPosition Camera = {202, 220};
|
||||
constexpr AppPosition Settings = {337, 220};
|
||||
constexpr AppPosition Music = {472, 220};
|
||||
|
||||
// Dock positions - 80px tall at bottom, items centered at y = height - 40
|
||||
// 4 items with space-around in 540px - 64px padding = 476px content
|
||||
// Each item 56px, total 224px, remaining 252px = ~63px per item spacing
|
||||
// Item centers: ~92, ~211, ~330, ~449
|
||||
// Note: Using 920 for Y (phone space), will be scaled by window size
|
||||
// Adjusted first item X slightly right due to hit testing issues
|
||||
constexpr AppPosition DockPhone = {100, 920};
|
||||
constexpr AppPosition DockMessages = {211, 920};
|
||||
constexpr AppPosition DockContacts = {330, 920};
|
||||
constexpr AppPosition DockBrowser = {449, 920};
|
||||
}
|
||||
|
||||
class TestRunner {
|
||||
public:
|
||||
TestRunner();
|
||||
~TestRunner();
|
||||
|
||||
// Configuration
|
||||
void SetDesignerPath(const std::filesystem::path& path);
|
||||
void SetDocumentPath(const std::filesystem::path& path);
|
||||
void SetLogPath(const std::filesystem::path& path);
|
||||
void SetHierarchyPath(const std::filesystem::path& path);
|
||||
void SetStartupTimeoutMs(int ms) { m_startupTimeoutMs = ms; }
|
||||
void SetActionDelayMs(int ms) { m_actionDelayMs = ms; }
|
||||
|
||||
// Add test cases
|
||||
void AddTest(const std::string& name, TestFunc func);
|
||||
|
||||
// Run all tests
|
||||
TestSuiteResult RunAll();
|
||||
|
||||
// Run a specific test by name
|
||||
TestResult RunTest(const std::string& name);
|
||||
|
||||
// Start/stop designer
|
||||
bool StartDesigner();
|
||||
void StopDesigner();
|
||||
|
||||
// Get components for manual testing
|
||||
WindowController& GetWindow() { return m_window; }
|
||||
LogParser& GetLog() { return m_log; }
|
||||
HierarchyReader& GetHierarchy() { return m_hierarchy; }
|
||||
|
||||
private:
|
||||
TestResult ExecuteTest(const TestCase& test);
|
||||
|
||||
std::filesystem::path m_designerPath;
|
||||
std::filesystem::path m_documentPath;
|
||||
std::filesystem::path m_logPath;
|
||||
std::filesystem::path m_hierarchyPath;
|
||||
|
||||
int m_startupTimeoutMs = 15000;
|
||||
int m_actionDelayMs = 500;
|
||||
|
||||
std::vector<TestCase> m_tests;
|
||||
WindowController m_window;
|
||||
LogParser m_log;
|
||||
HierarchyReader m_hierarchy;
|
||||
|
||||
HANDLE m_designerProcess = nullptr;
|
||||
};
|
||||
|
||||
} // namespace mosis::test
|
||||
265
designer-test/src/window_controller.cpp
Normal file
265
designer-test/src/window_controller.cpp
Normal file
@@ -0,0 +1,265 @@
|
||||
// Window controller implementation
|
||||
#include "window_controller.h"
|
||||
#include <iostream>
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
|
||||
namespace mosis::test {
|
||||
|
||||
bool WindowController::FindWindow(const std::string& title) {
|
||||
m_hwnd = ::FindWindowA(nullptr, title.c_str());
|
||||
if (!m_hwnd) {
|
||||
return false;
|
||||
}
|
||||
|
||||
m_info.hwnd = m_hwnd;
|
||||
|
||||
// Get window rect
|
||||
::GetWindowRect(m_hwnd, &m_info.windowRect);
|
||||
|
||||
// Get client rect (relative to window)
|
||||
RECT clientRect;
|
||||
::GetClientRect(m_hwnd, &clientRect);
|
||||
m_info.clientRect = clientRect;
|
||||
|
||||
// Convert client area origin to screen coordinates
|
||||
POINT clientOrigin = {0, 0};
|
||||
::ClientToScreen(m_hwnd, &clientOrigin);
|
||||
m_info.clientX = clientOrigin.x;
|
||||
m_info.clientY = clientOrigin.y;
|
||||
|
||||
// Calculate client size and DPI scale
|
||||
int clientWidth = clientRect.right - clientRect.left;
|
||||
int clientHeight = clientRect.bottom - clientRect.top;
|
||||
m_info.clientWidth = clientWidth;
|
||||
m_info.clientHeight = clientHeight;
|
||||
|
||||
m_info.scaleX = static_cast<float>(clientWidth) / PHONE_WIDTH;
|
||||
m_info.scaleY = static_cast<float>(clientHeight) / PHONE_HEIGHT;
|
||||
|
||||
std::cout << "Found window: " << title << std::endl;
|
||||
std::cout << " Window rect: " << m_info.windowRect.left << "," << m_info.windowRect.top
|
||||
<< " - " << m_info.windowRect.right << "," << m_info.windowRect.bottom << std::endl;
|
||||
std::cout << " Client origin: " << m_info.clientX << "," << m_info.clientY << std::endl;
|
||||
std::cout << " Client size: " << clientWidth << "x" << clientHeight << std::endl;
|
||||
std::cout << " DPI scale: " << m_info.scaleX << " x " << m_info.scaleY << std::endl;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WindowController::WaitForWindow(const std::string& title, int timeoutMs) {
|
||||
auto startTime = std::chrono::steady_clock::now();
|
||||
|
||||
while (true) {
|
||||
if (FindWindow(title)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
auto elapsed = std::chrono::steady_clock::now() - startTime;
|
||||
if (std::chrono::duration_cast<std::chrono::milliseconds>(elapsed).count() >= timeoutMs) {
|
||||
std::cerr << "Timeout waiting for window: " << title << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
}
|
||||
}
|
||||
|
||||
LPARAM WindowController::PhoneToClientLParam(int phoneX, int phoneY) {
|
||||
// Scale phone coordinates to client coordinates
|
||||
int clientX = static_cast<int>(phoneX * m_info.scaleX);
|
||||
int clientY = static_cast<int>(phoneY * m_info.scaleY);
|
||||
|
||||
// Clamp to client area bounds
|
||||
clientX = std::max(0, std::min(clientX, static_cast<int>(PHONE_WIDTH * m_info.scaleX) - 1));
|
||||
clientY = std::max(0, std::min(clientY, static_cast<int>(PHONE_HEIGHT * m_info.scaleY) - 1));
|
||||
|
||||
return MAKELPARAM(clientX, clientY);
|
||||
}
|
||||
|
||||
bool WindowController::SendMouseDown(int phoneX, int phoneY) {
|
||||
if (!m_hwnd) return false;
|
||||
|
||||
// Convert to screen coordinates for SendInput
|
||||
int clientX = static_cast<int>(phoneX * m_info.scaleX);
|
||||
int clientY = static_cast<int>(phoneY * m_info.scaleY);
|
||||
int screenX = m_info.clientX + clientX;
|
||||
int screenY = m_info.clientY + clientY;
|
||||
|
||||
// Use SendInput for proper GLFW compatibility
|
||||
// First move the cursor to the position
|
||||
SetCursorPos(screenX, screenY);
|
||||
|
||||
// Send mouse down via SendInput
|
||||
INPUT input = {};
|
||||
input.type = INPUT_MOUSE;
|
||||
input.mi.dwFlags = MOUSEEVENTF_LEFTDOWN;
|
||||
SendInput(1, &input, sizeof(INPUT));
|
||||
|
||||
std::cout << "MouseDown at phone(" << phoneX << "," << phoneY << ") -> screen("
|
||||
<< screenX << "," << screenY << ")" << std::endl;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WindowController::SendMouseUp(int phoneX, int phoneY) {
|
||||
if (!m_hwnd) return false;
|
||||
|
||||
// Send mouse up via SendInput
|
||||
INPUT input = {};
|
||||
input.type = INPUT_MOUSE;
|
||||
input.mi.dwFlags = MOUSEEVENTF_LEFTUP;
|
||||
SendInput(1, &input, sizeof(INPUT));
|
||||
|
||||
std::cout << "MouseUp at phone(" << phoneX << "," << phoneY << ")" << std::endl;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WindowController::SendMouseMove(int phoneX, int phoneY) {
|
||||
if (!m_hwnd) return false;
|
||||
|
||||
int clientX = static_cast<int>(phoneX * m_info.scaleX);
|
||||
int clientY = static_cast<int>(phoneY * m_info.scaleY);
|
||||
int screenX = m_info.clientX + clientX;
|
||||
int screenY = m_info.clientY + clientY;
|
||||
|
||||
SetCursorPos(screenX, screenY);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WindowController::SendClick(int phoneX, int phoneY) {
|
||||
if (!m_hwnd) return false;
|
||||
|
||||
// Save current cursor position
|
||||
POINT oldPos;
|
||||
GetCursorPos(&oldPos);
|
||||
|
||||
// Ensure window is active
|
||||
SetForegroundWindow(m_hwnd);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||
|
||||
// Perform click
|
||||
SendMouseDown(phoneX, phoneY);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||
SendMouseUp(phoneX, phoneY);
|
||||
|
||||
// Restore cursor position (optional - comment out if cursor restoration causes issues)
|
||||
// std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||
// SetCursorPos(oldPos.x, oldPos.y);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WindowController::SendKey(UINT vkCode) {
|
||||
if (!m_hwnd) return false;
|
||||
|
||||
// Send key down and up
|
||||
LPARAM lParam = 1; // Repeat count = 1
|
||||
::PostMessage(m_hwnd, WM_KEYDOWN, vkCode, lParam);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||
::PostMessage(m_hwnd, WM_KEYUP, vkCode, lParam | (1 << 30) | (1 << 31));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WindowController::SendChar(char c) {
|
||||
if (!m_hwnd) return false;
|
||||
|
||||
::PostMessage(m_hwnd, WM_CHAR, static_cast<WPARAM>(c), 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WindowController::Activate() {
|
||||
if (!m_hwnd) return false;
|
||||
|
||||
::SetForegroundWindow(m_hwnd);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WindowController::Close() {
|
||||
if (!m_hwnd) return false;
|
||||
|
||||
::PostMessage(m_hwnd, WM_CLOSE, 0, 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WindowController::ResizeToPhone() {
|
||||
if (!m_hwnd) return false;
|
||||
|
||||
// Get current window rect to preserve position
|
||||
RECT currentRect;
|
||||
::GetWindowRect(m_hwnd, ¤tRect);
|
||||
|
||||
// Calculate window size needed for phone-sized client area
|
||||
RECT desiredRect = {0, 0, PHONE_WIDTH, PHONE_HEIGHT};
|
||||
DWORD style = ::GetWindowLong(m_hwnd, GWL_STYLE);
|
||||
DWORD exStyle = ::GetWindowLong(m_hwnd, GWL_EXSTYLE);
|
||||
::AdjustWindowRectEx(&desiredRect, style, FALSE, exStyle);
|
||||
|
||||
int newWidth = desiredRect.right - desiredRect.left;
|
||||
int newHeight = desiredRect.bottom - desiredRect.top;
|
||||
|
||||
// Check screen dimensions
|
||||
int screenWidth = ::GetSystemMetrics(SM_CXSCREEN);
|
||||
int screenHeight = ::GetSystemMetrics(SM_CYSCREEN);
|
||||
|
||||
std::cout << "ResizeToPhone: screen=" << screenWidth << "x" << screenHeight
|
||||
<< ", needed=" << newWidth << "x" << newHeight << std::endl;
|
||||
|
||||
// If screen is too small, we can't resize to full phone size
|
||||
if (newHeight > screenHeight) {
|
||||
std::cout << " Warning: Screen too small for full phone resolution, keeping current size" << std::endl;
|
||||
return true; // Not an error, just can't resize
|
||||
}
|
||||
|
||||
// Move window to top-left to ensure it fits on screen
|
||||
int newX = 0;
|
||||
int newY = 0;
|
||||
|
||||
std::cout << " Moving to (" << newX << "," << newY << ") size " << newWidth << "x" << newHeight << std::endl;
|
||||
::MoveWindow(m_hwnd, newX, newY, newWidth, newHeight, TRUE);
|
||||
|
||||
// Re-acquire window info
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
return FindWindow("Mosis Designer");
|
||||
}
|
||||
|
||||
bool WindowController::SendClickFromBottom(int clientX, int offsetFromBottom) {
|
||||
if (!m_hwnd) return false;
|
||||
|
||||
// Calculate Y position from bottom of client area
|
||||
int clientY = m_info.clientHeight - offsetFromBottom;
|
||||
|
||||
// Convert to screen coordinates
|
||||
int screenX = m_info.clientX + clientX;
|
||||
int screenY = m_info.clientY + clientY;
|
||||
|
||||
// Save current cursor position
|
||||
POINT oldPos;
|
||||
GetCursorPos(&oldPos);
|
||||
|
||||
// Ensure window is active
|
||||
SetForegroundWindow(m_hwnd);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||
|
||||
// Move cursor and click
|
||||
SetCursorPos(screenX, screenY);
|
||||
|
||||
INPUT input = {};
|
||||
input.type = INPUT_MOUSE;
|
||||
input.mi.dwFlags = MOUSEEVENTF_LEFTDOWN;
|
||||
SendInput(1, &input, sizeof(INPUT));
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||
|
||||
input.mi.dwFlags = MOUSEEVENTF_LEFTUP;
|
||||
SendInput(1, &input, sizeof(INPUT));
|
||||
|
||||
std::cout << "ClickFromBottom at client(" << clientX << "," << clientY
|
||||
<< ") -> screen(" << screenX << "," << screenY << ")" << std::endl;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace mosis::test
|
||||
77
designer-test/src/window_controller.h
Normal file
77
designer-test/src/window_controller.h
Normal file
@@ -0,0 +1,77 @@
|
||||
// Window controller for sending input events to the designer
|
||||
#pragma once
|
||||
|
||||
#define NOMINMAX
|
||||
#include <Windows.h>
|
||||
#include <string>
|
||||
#include <optional>
|
||||
|
||||
namespace mosis::test {
|
||||
|
||||
// Phone UI dimensions (default resolution)
|
||||
constexpr int PHONE_WIDTH = 540;
|
||||
constexpr int PHONE_HEIGHT = 960;
|
||||
|
||||
// Window information
|
||||
struct WindowInfo {
|
||||
HWND hwnd;
|
||||
RECT windowRect;
|
||||
RECT clientRect;
|
||||
int clientX; // Client area X in screen coords
|
||||
int clientY; // Client area Y in screen coords
|
||||
int clientWidth; // Client area width
|
||||
int clientHeight; // Client area height
|
||||
float scaleX; // DPI scale X
|
||||
float scaleY; // DPI scale Y
|
||||
};
|
||||
|
||||
class WindowController {
|
||||
public:
|
||||
WindowController() = default;
|
||||
~WindowController() = default;
|
||||
|
||||
// Find the designer window by title
|
||||
bool FindWindow(const std::string& title = "Mosis Designer");
|
||||
|
||||
// Check if window is valid
|
||||
bool IsValid() const { return m_hwnd != nullptr; }
|
||||
|
||||
// Get window info
|
||||
const WindowInfo& GetInfo() const { return m_info; }
|
||||
|
||||
// Send mouse events at phone coordinates (0-540, 0-960)
|
||||
// These coordinates are scaled to match the actual window size
|
||||
bool SendMouseDown(int phoneX, int phoneY);
|
||||
bool SendMouseUp(int phoneX, int phoneY);
|
||||
bool SendMouseMove(int phoneX, int phoneY);
|
||||
bool SendClick(int phoneX, int phoneY);
|
||||
|
||||
// Send click at position relative to bottom of window
|
||||
// clientX is absolute X in client area, offsetFromBottom is Y offset from bottom edge
|
||||
bool SendClickFromBottom(int clientX, int offsetFromBottom);
|
||||
|
||||
// Send keyboard events
|
||||
bool SendKey(UINT vkCode);
|
||||
bool SendChar(char c);
|
||||
|
||||
// Bring window to foreground
|
||||
bool Activate();
|
||||
|
||||
// Close the window
|
||||
bool Close();
|
||||
|
||||
// Resize window to match phone dimensions
|
||||
bool ResizeToPhone();
|
||||
|
||||
// Wait for window to appear (returns false on timeout)
|
||||
bool WaitForWindow(const std::string& title, int timeoutMs = 10000);
|
||||
|
||||
private:
|
||||
// Convert phone coordinates to client coordinates (accounting for DPI scale)
|
||||
LPARAM PhoneToClientLParam(int phoneX, int phoneY);
|
||||
|
||||
HWND m_hwnd = nullptr;
|
||||
WindowInfo m_info = {};
|
||||
};
|
||||
|
||||
} // namespace mosis::test
|
||||
7
designer-test/vcpkg.json
Normal file
7
designer-test/vcpkg.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "designer-test",
|
||||
"version-string": "0.1.0",
|
||||
"dependencies": [
|
||||
"nlohmann-json"
|
||||
]
|
||||
}
|
||||
104
designer/CMakeLists.txt
Normal file
104
designer/CMakeLists.txt
Normal file
@@ -0,0 +1,104 @@
|
||||
cmake_minimum_required(VERSION 3.22.1)
|
||||
project(mosis-designer)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 23)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
# Find Lua before RmlUi so it can be used
|
||||
find_package(Lua REQUIRED)
|
||||
|
||||
# Find other dependencies via vcpkg
|
||||
find_package(glfw3 CONFIG REQUIRED)
|
||||
find_package(freetype CONFIG REQUIRED)
|
||||
find_package(PNG REQUIRED)
|
||||
find_package(nlohmann_json CONFIG REQUIRED)
|
||||
|
||||
# Fetch RmlUi
|
||||
include(FetchContent)
|
||||
FetchContent_Declare(
|
||||
rmlui
|
||||
GIT_REPOSITORY https://github.com/mikke89/RmlUi.git
|
||||
GIT_TAG 6.0
|
||||
)
|
||||
|
||||
# Enable RmlUi Lua bindings before fetching
|
||||
set(RMLUI_LUA_BINDINGS ON CACHE BOOL "" FORCE)
|
||||
set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
|
||||
set(RMLUI_SAMPLES OFF CACHE BOOL "" FORCE)
|
||||
set(RMLUI_TESTS OFF CACHE BOOL "" FORCE)
|
||||
set(RMLUI_FONT_ENGINE "freetype" CACHE STRING "" FORCE)
|
||||
|
||||
FetchContent_MakeAvailable(rmlui)
|
||||
|
||||
# Get the RmlUi source directory for backend sources
|
||||
FetchContent_GetProperties(rmlui)
|
||||
set(RMLUI_SOURCE_DIR ${rmlui_SOURCE_DIR})
|
||||
|
||||
# Shared kernel library sources (platform-agnostic code)
|
||||
set(KERNEL_SOURCES
|
||||
../src/main/kernel/src/platform.cpp
|
||||
../src/main/kernel/src/file_interface.cpp
|
||||
)
|
||||
|
||||
# Desktop platform sources
|
||||
set(DESIGNER_SOURCES
|
||||
src/main.cpp
|
||||
src/desktop_platform.cpp
|
||||
src/hot_reload.cpp
|
||||
src/data_models.cpp
|
||||
src/kernel_impl.cpp
|
||||
src/testing/action_recorder.cpp
|
||||
src/testing/action_player.cpp
|
||||
src/testing/ui_inspector.cpp
|
||||
src/testing/visual_capture.cpp
|
||||
# RmlUi backend sources
|
||||
${RMLUI_SOURCE_DIR}/Backends/RmlUi_Backend_GLFW_GL3.cpp
|
||||
${RMLUI_SOURCE_DIR}/Backends/RmlUi_Platform_GLFW.cpp
|
||||
${RMLUI_SOURCE_DIR}/Backends/RmlUi_Renderer_GL3.cpp
|
||||
)
|
||||
|
||||
# Designer executable
|
||||
add_executable(mosis-designer
|
||||
${KERNEL_SOURCES}
|
||||
${DESIGNER_SOURCES}
|
||||
)
|
||||
|
||||
target_include_directories(mosis-designer PRIVATE
|
||||
src
|
||||
../src/main/kernel/include
|
||||
${RMLUI_SOURCE_DIR}
|
||||
${RMLUI_SOURCE_DIR}/Backends
|
||||
${LUA_INCLUDE_DIR}
|
||||
)
|
||||
|
||||
target_link_libraries(mosis-designer PRIVATE
|
||||
glfw
|
||||
freetype
|
||||
PNG::PNG
|
||||
RmlUi::RmlUi
|
||||
RmlUi::Lua
|
||||
nlohmann_json::nlohmann_json
|
||||
)
|
||||
|
||||
target_compile_definitions(mosis-designer PRIVATE
|
||||
MOSIS_PLATFORM_DESKTOP
|
||||
RMLUI_STATIC_LIB
|
||||
)
|
||||
|
||||
# Platform-specific libraries
|
||||
if(WIN32)
|
||||
target_link_libraries(mosis-designer PRIVATE opengl32)
|
||||
elseif(APPLE)
|
||||
find_library(OPENGL_LIBRARY OpenGL)
|
||||
target_link_libraries(mosis-designer PRIVATE ${OPENGL_LIBRARY})
|
||||
else()
|
||||
find_package(OpenGL REQUIRED)
|
||||
target_link_libraries(mosis-designer PRIVATE OpenGL::GL)
|
||||
endif()
|
||||
|
||||
# Copy assets for development
|
||||
add_custom_command(TARGET mosis-designer POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../src/main/assets
|
||||
$<TARGET_FILE_DIR:mosis-designer>/assets
|
||||
)
|
||||
137
designer/glad/include/KHR/khrplatform.h
Normal file
137
designer/glad/include/KHR/khrplatform.h
Normal file
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
** Copyright (c) 2008-2018 The Khronos Group Inc.
|
||||
**
|
||||
** Permission is hereby granted, free of charge, to any person obtaining a
|
||||
** copy of this software and/or associated documentation files (the
|
||||
** "Materials"), to deal in the Materials without restriction, including
|
||||
** without limitation the rights to use, copy, modify, merge, publish,
|
||||
** distribute, sublicense, and/or sell copies of the Materials, and to
|
||||
** permit persons to whom the Materials are 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 Materials.
|
||||
**
|
||||
** THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
** IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
** CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
** TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
** MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS.
|
||||
*/
|
||||
|
||||
#ifndef __khrplatform_h_
|
||||
#define __khrplatform_h_
|
||||
|
||||
#if defined(__SCITECH_SNAP__) && !defined(KHRONOS_STATIC)
|
||||
# define KHRONOS_STATIC 1
|
||||
#endif
|
||||
|
||||
#if defined(KHRONOS_STATIC)
|
||||
# define KHRONOS_APICALL
|
||||
#elif defined(_WIN32)
|
||||
# define KHRONOS_APICALL __declspec(dllimport)
|
||||
#elif defined (__SYMBIAN32__)
|
||||
# define KHRONOS_APICALL IMPORT_C
|
||||
#elif defined(__ANDROID__)
|
||||
# define KHRONOS_APICALL __attribute__((visibility("default")))
|
||||
#else
|
||||
# define KHRONOS_APICALL
|
||||
#endif
|
||||
|
||||
#if defined(_WIN32) && !defined(_WIN32_WCE) && !defined(KHRONOS_STATIC)
|
||||
# define KHRONOS_APIENTRY __stdcall
|
||||
#else
|
||||
# define KHRONOS_APIENTRY
|
||||
#endif
|
||||
|
||||
#if defined(KHRONOS_SUPPORT_INT64)
|
||||
typedef int64_t khronos_int64_t;
|
||||
typedef uint64_t khronos_uint64_t;
|
||||
#define KHRONOS_SUPPORT_INT64 1
|
||||
#define KHRONOS_SUPPORT_FLOAT 1
|
||||
#elif defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L
|
||||
#include <stdint.h>
|
||||
typedef int32_t khronos_int32_t;
|
||||
typedef uint32_t khronos_uint32_t;
|
||||
typedef int64_t khronos_int64_t;
|
||||
typedef uint64_t khronos_uint64_t;
|
||||
#define KHRONOS_SUPPORT_INT64 1
|
||||
#define KHRONOS_SUPPORT_FLOAT 1
|
||||
|
||||
#elif defined(__cplusplus)
|
||||
#include <cstdint>
|
||||
typedef int32_t khronos_int32_t;
|
||||
typedef uint32_t khronos_uint32_t;
|
||||
typedef int64_t khronos_int64_t;
|
||||
typedef uint64_t khronos_uint64_t;
|
||||
#define KHRONOS_SUPPORT_INT64 1
|
||||
#define KHRONOS_SUPPORT_FLOAT 1
|
||||
|
||||
#elif defined(_WIN32) && !defined(__SCITECH_SNAP__)
|
||||
typedef __int32 khronos_int32_t;
|
||||
typedef unsigned __int32 khronos_uint32_t;
|
||||
typedef __int64 khronos_int64_t;
|
||||
typedef unsigned __int64 khronos_uint64_t;
|
||||
#define KHRONOS_SUPPORT_INT64 1
|
||||
#define KHRONOS_SUPPORT_FLOAT 1
|
||||
|
||||
#elif defined(__sun__) || defined(__digital__)
|
||||
typedef int khronos_int32_t;
|
||||
typedef unsigned int khronos_uint32_t;
|
||||
#if defined(__arch64__) || defined(_LP64)
|
||||
typedef long int khronos_int64_t;
|
||||
typedef unsigned long int khronos_uint64_t;
|
||||
#else
|
||||
typedef long long int khronos_int64_t;
|
||||
typedef unsigned long long int khronos_uint64_t;
|
||||
#endif
|
||||
#define KHRONOS_SUPPORT_INT64 1
|
||||
#define KHRONOS_SUPPORT_FLOAT 1
|
||||
|
||||
#elif 0
|
||||
typedef int khronos_int32_t;
|
||||
typedef unsigned int khronos_uint32_t;
|
||||
typedef long long int khronos_int64_t;
|
||||
typedef unsigned long long int khronos_uint64_t;
|
||||
#define KHRONOS_SUPPORT_INT64 1
|
||||
#define KHRONOS_SUPPORT_FLOAT 1
|
||||
|
||||
#else
|
||||
typedef int khronos_int32_t;
|
||||
typedef unsigned int khronos_uint32_t;
|
||||
typedef long long int khronos_int64_t;
|
||||
typedef unsigned long long int khronos_uint64_t;
|
||||
#define KHRONOS_SUPPORT_INT64 1
|
||||
#define KHRONOS_SUPPORT_FLOAT 1
|
||||
#endif
|
||||
|
||||
typedef signed char khronos_int8_t;
|
||||
typedef unsigned char khronos_uint8_t;
|
||||
typedef signed short int khronos_int16_t;
|
||||
typedef unsigned short int khronos_uint16_t;
|
||||
|
||||
#ifdef KHRONOS_SUPPORT_FLOAT
|
||||
typedef float khronos_float_t;
|
||||
#endif
|
||||
|
||||
#if KHRONOS_SUPPORT_INT64
|
||||
typedef khronos_uint64_t khronos_utime_nanoseconds_t;
|
||||
typedef khronos_int64_t khronos_stime_nanoseconds_t;
|
||||
#endif
|
||||
|
||||
#ifndef KHRONOS_MAX_ENUM
|
||||
#define KHRONOS_MAX_ENUM 0x7FFFFFFF
|
||||
#endif
|
||||
|
||||
typedef enum {
|
||||
KHRONOS_FALSE = 0,
|
||||
KHRONOS_TRUE = 1,
|
||||
KHRONOS_BOOLEAN_ENUM_FORCE_SIZE = KHRONOS_MAX_ENUM
|
||||
} khronos_boolean_enum_t;
|
||||
|
||||
typedef khronos_uint64_t khronos_usize_t;
|
||||
typedef khronos_int64_t khronos_ssize_t;
|
||||
|
||||
#endif /* __khrplatform_h_ */
|
||||
623
designer/glad/include/glad/gl.h
Normal file
623
designer/glad/include/glad/gl.h
Normal file
@@ -0,0 +1,623 @@
|
||||
/*
|
||||
* GLAD OpenGL 3.3 Core Profile Loader
|
||||
* Generated for Mosis Designer
|
||||
*/
|
||||
|
||||
#ifndef GLAD_GL_H_
|
||||
#define GLAD_GL_H_
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include <KHR/khrplatform.h>
|
||||
|
||||
#define GLAD_VERSION_MAJOR 2
|
||||
#define GLAD_VERSION_MINOR 0
|
||||
|
||||
#ifndef GLAD_API_CALL
|
||||
#if defined(_WIN32) || defined(__CYGWIN__)
|
||||
# define GLAD_API_CALL __declspec(dllexport)
|
||||
#else
|
||||
# define GLAD_API_CALL __attribute__((visibility("default")))
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#define GLAD_MAKE_VERSION(major, minor) (major * 10000 + minor)
|
||||
#define GLAD_VERSION GLAD_MAKE_VERSION(GLAD_VERSION_MAJOR, GLAD_VERSION_MINOR)
|
||||
|
||||
typedef void (*GLADapiproc)(void);
|
||||
typedef GLADapiproc (*GLADloadfunc)(const char *name);
|
||||
|
||||
GLAD_API_CALL int gladLoadGL(GLADloadfunc load);
|
||||
GLAD_API_CALL int gladLoadGLUserPtr(GLADloadfunc load, void *userptr);
|
||||
|
||||
/* GL Types */
|
||||
typedef void GLvoid;
|
||||
typedef unsigned int GLenum;
|
||||
typedef float GLfloat;
|
||||
typedef int GLint;
|
||||
typedef int GLsizei;
|
||||
typedef unsigned int GLbitfield;
|
||||
typedef double GLdouble;
|
||||
typedef unsigned int GLuint;
|
||||
typedef unsigned char GLboolean;
|
||||
typedef khronos_uint8_t GLubyte;
|
||||
typedef khronos_int8_t GLbyte;
|
||||
typedef khronos_int16_t GLshort;
|
||||
typedef khronos_uint16_t GLushort;
|
||||
typedef khronos_float_t GLclampf;
|
||||
typedef double GLclampd;
|
||||
typedef khronos_ssize_t GLsizeiptr;
|
||||
typedef khronos_intptr_t GLintptr;
|
||||
typedef char GLchar;
|
||||
typedef khronos_int64_t GLint64;
|
||||
typedef khronos_uint64_t GLuint64;
|
||||
|
||||
#ifndef __gl_glcorearb_h_
|
||||
|
||||
/* GL Constants */
|
||||
#define GL_FALSE 0
|
||||
#define GL_TRUE 1
|
||||
#define GL_NONE 0
|
||||
#define GL_ZERO 0
|
||||
#define GL_ONE 1
|
||||
|
||||
/* Primitives */
|
||||
#define GL_POINTS 0x0000
|
||||
#define GL_LINES 0x0001
|
||||
#define GL_LINE_LOOP 0x0002
|
||||
#define GL_LINE_STRIP 0x0003
|
||||
#define GL_TRIANGLES 0x0004
|
||||
#define GL_TRIANGLE_STRIP 0x0005
|
||||
#define GL_TRIANGLE_FAN 0x0006
|
||||
|
||||
/* Depth */
|
||||
#define GL_DEPTH_BUFFER_BIT 0x00000100
|
||||
#define GL_STENCIL_BUFFER_BIT 0x00000400
|
||||
#define GL_COLOR_BUFFER_BIT 0x00004000
|
||||
#define GL_DEPTH_TEST 0x0B71
|
||||
#define GL_DEPTH_FUNC 0x0B74
|
||||
#define GL_DEPTH_WRITEMASK 0x0B72
|
||||
|
||||
/* Stencil */
|
||||
#define GL_STENCIL_TEST 0x0B90
|
||||
#define GL_STENCIL_FUNC 0x0B92
|
||||
#define GL_STENCIL_VALUE_MASK 0x0B93
|
||||
#define GL_STENCIL_REF 0x0B97
|
||||
#define GL_STENCIL_FAIL 0x0B94
|
||||
#define GL_STENCIL_PASS_DEPTH_FAIL 0x0B95
|
||||
#define GL_STENCIL_PASS_DEPTH_PASS 0x0B96
|
||||
#define GL_KEEP 0x1E00
|
||||
#define GL_REPLACE 0x1E01
|
||||
#define GL_INCR 0x1E02
|
||||
#define GL_DECR 0x1E03
|
||||
#define GL_INVERT 0x150A
|
||||
#define GL_INCR_WRAP 0x8507
|
||||
#define GL_DECR_WRAP 0x8508
|
||||
|
||||
/* Blending */
|
||||
#define GL_BLEND 0x0BE2
|
||||
#define GL_SRC_COLOR 0x0300
|
||||
#define GL_ONE_MINUS_SRC_COLOR 0x0301
|
||||
#define GL_SRC_ALPHA 0x0302
|
||||
#define GL_ONE_MINUS_SRC_ALPHA 0x0303
|
||||
#define GL_DST_ALPHA 0x0304
|
||||
#define GL_ONE_MINUS_DST_ALPHA 0x0305
|
||||
#define GL_DST_COLOR 0x0306
|
||||
#define GL_ONE_MINUS_DST_COLOR 0x0307
|
||||
#define GL_SRC_ALPHA_SATURATE 0x0308
|
||||
#define GL_CONSTANT_COLOR 0x8001
|
||||
#define GL_ONE_MINUS_CONSTANT_COLOR 0x8002
|
||||
#define GL_CONSTANT_ALPHA 0x8003
|
||||
#define GL_ONE_MINUS_CONSTANT_ALPHA 0x8004
|
||||
#define GL_FUNC_ADD 0x8006
|
||||
#define GL_MIN 0x8007
|
||||
#define GL_MAX 0x8008
|
||||
#define GL_FUNC_SUBTRACT 0x800A
|
||||
#define GL_FUNC_REVERSE_SUBTRACT 0x800B
|
||||
|
||||
/* Culling */
|
||||
#define GL_CULL_FACE 0x0B44
|
||||
#define GL_FRONT 0x0404
|
||||
#define GL_BACK 0x0405
|
||||
#define GL_FRONT_AND_BACK 0x0408
|
||||
#define GL_CW 0x0900
|
||||
#define GL_CCW 0x0901
|
||||
|
||||
/* Comparison */
|
||||
#define GL_NEVER 0x0200
|
||||
#define GL_LESS 0x0201
|
||||
#define GL_EQUAL 0x0202
|
||||
#define GL_LEQUAL 0x0203
|
||||
#define GL_GREATER 0x0204
|
||||
#define GL_NOTEQUAL 0x0205
|
||||
#define GL_GEQUAL 0x0206
|
||||
#define GL_ALWAYS 0x0207
|
||||
|
||||
/* Errors */
|
||||
#define GL_NO_ERROR 0
|
||||
#define GL_INVALID_ENUM 0x0500
|
||||
#define GL_INVALID_VALUE 0x0501
|
||||
#define GL_INVALID_OPERATION 0x0502
|
||||
#define GL_OUT_OF_MEMORY 0x0505
|
||||
#define GL_INVALID_FRAMEBUFFER_OPERATION 0x0506
|
||||
|
||||
/* Data types */
|
||||
#define GL_BYTE 0x1400
|
||||
#define GL_UNSIGNED_BYTE 0x1401
|
||||
#define GL_SHORT 0x1402
|
||||
#define GL_UNSIGNED_SHORT 0x1403
|
||||
#define GL_INT 0x1404
|
||||
#define GL_UNSIGNED_INT 0x1405
|
||||
#define GL_FLOAT 0x1406
|
||||
#define GL_DOUBLE 0x140A
|
||||
#define GL_HALF_FLOAT 0x140B
|
||||
|
||||
/* Gets */
|
||||
#define GL_VIEWPORT 0x0BA2
|
||||
#define GL_SCISSOR_BOX 0x0C10
|
||||
#define GL_SCISSOR_TEST 0x0C11
|
||||
#define GL_MAX_TEXTURE_SIZE 0x0D33
|
||||
#define GL_MAX_VIEWPORT_DIMS 0x0D3A
|
||||
#define GL_PACK_ALIGNMENT 0x0D05
|
||||
#define GL_UNPACK_ALIGNMENT 0x0CF5
|
||||
|
||||
/* Textures */
|
||||
#define GL_TEXTURE_1D 0x0DE0
|
||||
#define GL_TEXTURE_2D 0x0DE1
|
||||
#define GL_TEXTURE_3D 0x806F
|
||||
#define GL_TEXTURE_CUBE_MAP 0x8513
|
||||
#define GL_TEXTURE_2D_ARRAY 0x8C1A
|
||||
#define GL_TEXTURE_2D_MULTISAMPLE 0x9100
|
||||
#define GL_TEXTURE_BINDING_2D 0x8069
|
||||
#define GL_TEXTURE_WIDTH 0x1000
|
||||
#define GL_TEXTURE_HEIGHT 0x1001
|
||||
#define GL_TEXTURE_INTERNAL_FORMAT 0x1003
|
||||
#define GL_TEXTURE_MAG_FILTER 0x2800
|
||||
#define GL_TEXTURE_MIN_FILTER 0x2801
|
||||
#define GL_TEXTURE_WRAP_S 0x2802
|
||||
#define GL_TEXTURE_WRAP_T 0x2803
|
||||
#define GL_TEXTURE_WRAP_R 0x8072
|
||||
#define GL_TEXTURE0 0x84C0
|
||||
#define GL_TEXTURE1 0x84C1
|
||||
#define GL_TEXTURE2 0x84C2
|
||||
#define GL_TEXTURE3 0x84C3
|
||||
#define GL_TEXTURE4 0x84C4
|
||||
#define GL_TEXTURE5 0x84C5
|
||||
#define GL_TEXTURE6 0x84C6
|
||||
#define GL_TEXTURE7 0x84C7
|
||||
#define GL_TEXTURE8 0x84C8
|
||||
#define GL_TEXTURE9 0x84C9
|
||||
#define GL_TEXTURE10 0x84CA
|
||||
#define GL_ACTIVE_TEXTURE 0x84E0
|
||||
#define GL_MAX_TEXTURE_IMAGE_UNITS 0x8872
|
||||
#define GL_NEAREST 0x2600
|
||||
#define GL_LINEAR 0x2601
|
||||
#define GL_NEAREST_MIPMAP_NEAREST 0x2700
|
||||
#define GL_LINEAR_MIPMAP_NEAREST 0x2701
|
||||
#define GL_NEAREST_MIPMAP_LINEAR 0x2702
|
||||
#define GL_LINEAR_MIPMAP_LINEAR 0x2703
|
||||
#define GL_REPEAT 0x2901
|
||||
#define GL_CLAMP_TO_EDGE 0x812F
|
||||
#define GL_CLAMP_TO_BORDER 0x812D
|
||||
#define GL_MIRRORED_REPEAT 0x8370
|
||||
|
||||
/* Pixel formats */
|
||||
#define GL_RED 0x1903
|
||||
#define GL_GREEN 0x1904
|
||||
#define GL_BLUE 0x1905
|
||||
#define GL_ALPHA 0x1906
|
||||
#define GL_RGB 0x1907
|
||||
#define GL_RGBA 0x1908
|
||||
#define GL_LUMINANCE 0x1909
|
||||
#define GL_LUMINANCE_ALPHA 0x190A
|
||||
#define GL_BGR 0x80E0
|
||||
#define GL_BGRA 0x80E1
|
||||
#define GL_R8 0x8229
|
||||
#define GL_RG8 0x822B
|
||||
#define GL_RGB8 0x8051
|
||||
#define GL_RGBA8 0x8058
|
||||
#define GL_SRGB8 0x8C41
|
||||
#define GL_SRGB8_ALPHA8 0x8C43
|
||||
#define GL_DEPTH_COMPONENT 0x1902
|
||||
#define GL_DEPTH_COMPONENT16 0x81A5
|
||||
#define GL_DEPTH_COMPONENT24 0x81A6
|
||||
#define GL_DEPTH_COMPONENT32 0x81A7
|
||||
#define GL_DEPTH_COMPONENT32F 0x8CAC
|
||||
#define GL_DEPTH24_STENCIL8 0x88F0
|
||||
#define GL_DEPTH32F_STENCIL8 0x8CAD
|
||||
#define GL_DEPTH_STENCIL 0x84F9
|
||||
|
||||
/* Shaders */
|
||||
#define GL_VERTEX_SHADER 0x8B31
|
||||
#define GL_FRAGMENT_SHADER 0x8B30
|
||||
#define GL_GEOMETRY_SHADER 0x8DD9
|
||||
#define GL_COMPILE_STATUS 0x8B81
|
||||
#define GL_LINK_STATUS 0x8B82
|
||||
#define GL_INFO_LOG_LENGTH 0x8B84
|
||||
#define GL_CURRENT_PROGRAM 0x8B8D
|
||||
#define GL_SHADER_TYPE 0x8B4F
|
||||
#define GL_DELETE_STATUS 0x8B80
|
||||
#define GL_ATTACHED_SHADERS 0x8B85
|
||||
#define GL_ACTIVE_UNIFORMS 0x8B86
|
||||
#define GL_ACTIVE_UNIFORM_MAX_LENGTH 0x8B87
|
||||
#define GL_ACTIVE_ATTRIBUTES 0x8B89
|
||||
#define GL_ACTIVE_ATTRIBUTE_MAX_LENGTH 0x8B8A
|
||||
|
||||
/* Buffers */
|
||||
#define GL_ARRAY_BUFFER 0x8892
|
||||
#define GL_ELEMENT_ARRAY_BUFFER 0x8893
|
||||
#define GL_UNIFORM_BUFFER 0x8A11
|
||||
#define GL_STREAM_DRAW 0x88E0
|
||||
#define GL_STREAM_READ 0x88E1
|
||||
#define GL_STREAM_COPY 0x88E2
|
||||
#define GL_STATIC_DRAW 0x88E4
|
||||
#define GL_STATIC_READ 0x88E5
|
||||
#define GL_STATIC_COPY 0x88E6
|
||||
#define GL_DYNAMIC_DRAW 0x88E8
|
||||
#define GL_DYNAMIC_READ 0x88E9
|
||||
#define GL_DYNAMIC_COPY 0x88EA
|
||||
#define GL_BUFFER_SIZE 0x8764
|
||||
#define GL_BUFFER_USAGE 0x8765
|
||||
#define GL_BUFFER_MAPPED 0x88BC
|
||||
#define GL_BUFFER_MAP_POINTER 0x88BD
|
||||
|
||||
/* VAOs */
|
||||
#define GL_VERTEX_ARRAY_BINDING 0x85B5
|
||||
|
||||
/* Framebuffers */
|
||||
#define GL_FRAMEBUFFER 0x8D40
|
||||
#define GL_READ_FRAMEBUFFER 0x8CA8
|
||||
#define GL_DRAW_FRAMEBUFFER 0x8CA9
|
||||
#define GL_RENDERBUFFER 0x8D41
|
||||
#define GL_COLOR_ATTACHMENT0 0x8CE0
|
||||
#define GL_COLOR_ATTACHMENT1 0x8CE1
|
||||
#define GL_COLOR_ATTACHMENT2 0x8CE2
|
||||
#define GL_COLOR_ATTACHMENT3 0x8CE3
|
||||
#define GL_DEPTH_ATTACHMENT 0x8D00
|
||||
#define GL_STENCIL_ATTACHMENT 0x8D20
|
||||
#define GL_DEPTH_STENCIL_ATTACHMENT 0x821A
|
||||
#define GL_FRAMEBUFFER_COMPLETE 0x8CD5
|
||||
#define GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT 0x8CD6
|
||||
#define GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT 0x8CD7
|
||||
#define GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER 0x8CDB
|
||||
#define GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER 0x8CDC
|
||||
#define GL_FRAMEBUFFER_UNSUPPORTED 0x8CDD
|
||||
#define GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE 0x8D56
|
||||
#define GL_MAX_COLOR_ATTACHMENTS 0x8CDF
|
||||
#define GL_MAX_RENDERBUFFER_SIZE 0x84E8
|
||||
#define GL_MAX_SAMPLES 0x8D57
|
||||
|
||||
/* Multisample */
|
||||
#define GL_MULTISAMPLE 0x809D
|
||||
#define GL_SAMPLE_ALPHA_TO_COVERAGE 0x809E
|
||||
#define GL_SAMPLE_ALPHA_TO_ONE 0x809F
|
||||
#define GL_SAMPLE_COVERAGE 0x80A0
|
||||
|
||||
/* Misc */
|
||||
#define GL_VENDOR 0x1F00
|
||||
#define GL_RENDERER 0x1F01
|
||||
#define GL_VERSION 0x1F02
|
||||
#define GL_EXTENSIONS 0x1F03
|
||||
#define GL_SHADING_LANGUAGE_VERSION 0x8B8C
|
||||
#define GL_DITHER 0x0BD0
|
||||
#define GL_LINE_SMOOTH 0x0B20
|
||||
#define GL_POLYGON_SMOOTH 0x0B41
|
||||
#define GL_POLYGON_OFFSET_FILL 0x8037
|
||||
#define GL_PROGRAM_POINT_SIZE 0x8642
|
||||
|
||||
#endif /* __gl_glcorearb_h_ */
|
||||
|
||||
/* Version flags */
|
||||
#define GLAD_GL 1
|
||||
#define GLAD_GL_VERSION_1_0 1
|
||||
#define GLAD_GL_VERSION_1_1 1
|
||||
#define GLAD_GL_VERSION_1_2 1
|
||||
#define GLAD_GL_VERSION_1_3 1
|
||||
#define GLAD_GL_VERSION_1_4 1
|
||||
#define GLAD_GL_VERSION_1_5 1
|
||||
#define GLAD_GL_VERSION_2_0 1
|
||||
#define GLAD_GL_VERSION_2_1 1
|
||||
#define GLAD_GL_VERSION_3_0 1
|
||||
#define GLAD_GL_VERSION_3_1 1
|
||||
#define GLAD_GL_VERSION_3_2 1
|
||||
#define GLAD_GL_VERSION_3_3 1
|
||||
|
||||
/* Function pointer types */
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLACTIVETEXTUREPROC)(GLenum texture);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLATTACHSHADERPROC)(GLuint program, GLuint shader);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLBINDBUFFERPROC)(GLenum target, GLuint buffer);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLBINDBUFFERBASEPROC)(GLenum target, GLuint index, GLuint buffer);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLBINDFRAMEBUFFERPROC)(GLenum target, GLuint framebuffer);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLBINDRENDERBUFFERPROC)(GLenum target, GLuint renderbuffer);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLBINDTEXTUREPROC)(GLenum target, GLuint texture);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLBINDVERTEXARRAYPROC)(GLuint array);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLBLENDCOLORPROC)(GLfloat red, GLfloat green, GLfloat blue, GLfloat alpha);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLBLENDEQUATIONPROC)(GLenum mode);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLBLENDEQUATIONSEPARATEPROC)(GLenum modeRGB, GLenum modeAlpha);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLBLENDFUNCPROC)(GLenum sfactor, GLenum dfactor);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLBLENDFUNCSEPARATEPROC)(GLenum srcRGB, GLenum dstRGB, GLenum srcAlpha, GLenum dstAlpha);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLBLITFRAMEBUFFERPROC)(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLBUFFERDATAPROC)(GLenum target, GLsizeiptr size, const void *data, GLenum usage);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLBUFFERSUBDATAPROC)(GLenum target, GLintptr offset, GLsizeiptr size, const void *data);
|
||||
typedef GLenum (KHRONOS_APIENTRY *PFNGLCHECKFRAMEBUFFERSTATUSPROC)(GLenum target);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLCLEARPROC)(GLbitfield mask);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLCLEARCOLORPROC)(GLfloat red, GLfloat green, GLfloat blue, GLfloat alpha);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLCLEARDEPTHPROC)(GLdouble depth);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLCLEARSTENCILPROC)(GLint s);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLCOLORMASKPROC)(GLboolean red, GLboolean green, GLboolean blue, GLboolean alpha);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLCOMPILESHADERPROC)(GLuint shader);
|
||||
typedef GLuint (KHRONOS_APIENTRY *PFNGLCREATEPROGRAMPROC)(void);
|
||||
typedef GLuint (KHRONOS_APIENTRY *PFNGLCREATESHADERPROC)(GLenum type);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLCULLFACEPROC)(GLenum mode);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLDELETEBUFFERSPROC)(GLsizei n, const GLuint *buffers);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLDELETEFRAMEBUFFERSPROC)(GLsizei n, const GLuint *framebuffers);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLDELETEPROGRAMPROC)(GLuint program);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLDELETERENDERBUFFERSPROC)(GLsizei n, const GLuint *renderbuffers);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLDELETESHADERPROC)(GLuint shader);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLDELETETEXTURESPROC)(GLsizei n, const GLuint *textures);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLDELETEVERTEXARRAYSPROC)(GLsizei n, const GLuint *arrays);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLDEPTHFUNCPROC)(GLenum func);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLDEPTHMASKPROC)(GLboolean flag);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLDISABLEPROC)(GLenum cap);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLDISABLEVERTEXATTRIBARRAYPROC)(GLuint index);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLDRAWARRAYSPROC)(GLenum mode, GLint first, GLsizei count);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLDRAWBUFFERSPROC)(GLsizei n, const GLenum *bufs);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLDRAWELEMENTSPROC)(GLenum mode, GLsizei count, GLenum type, const void *indices);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLENABLEPROC)(GLenum cap);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLENABLEVERTEXATTRIBARRAYPROC)(GLuint index);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLFINISHPROC)(void);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLFLUSHPROC)(void);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLFRAMEBUFFERRENDERBUFFERPROC)(GLenum target, GLenum attachment, GLenum renderbuffertarget, GLuint renderbuffer);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLFRAMEBUFFERTEXTURE2DPROC)(GLenum target, GLenum attachment, GLenum textarget, GLuint texture, GLint level);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLFRONTFACEPROC)(GLenum mode);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLGENBUFFERSPROC)(GLsizei n, GLuint *buffers);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLGENFRAMEBUFFERSPROC)(GLsizei n, GLuint *framebuffers);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLGENRENDERBUFFERSPROC)(GLsizei n, GLuint *renderbuffers);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLGENTEXTURESPROC)(GLsizei n, GLuint *textures);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLGENVERTEXARRAYSPROC)(GLsizei n, GLuint *arrays);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLGENERATEMIPMAPPROC)(GLenum target);
|
||||
typedef GLint (KHRONOS_APIENTRY *PFNGLGETATTRIBLOCATIONPROC)(GLuint program, const GLchar *name);
|
||||
typedef GLenum (KHRONOS_APIENTRY *PFNGLGETERRORPROC)(void);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLGETINTEGERVPROC)(GLenum pname, GLint *data);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLGETPROGRAMINFOLOGPROC)(GLuint program, GLsizei bufSize, GLsizei *length, GLchar *infoLog);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLGETPROGRAMIVPROC)(GLuint program, GLenum pname, GLint *params);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLGETSHADERINFOLOGPROC)(GLuint shader, GLsizei bufSize, GLsizei *length, GLchar *infoLog);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLGETSHADERIVPROC)(GLuint shader, GLenum pname, GLint *params);
|
||||
typedef const GLubyte * (KHRONOS_APIENTRY *PFNGLGETSTRINGPROC)(GLenum name);
|
||||
typedef const GLubyte * (KHRONOS_APIENTRY *PFNGLGETSTRINGIPROC)(GLenum name, GLuint index);
|
||||
typedef GLint (KHRONOS_APIENTRY *PFNGLGETUNIFORMLOCATIONPROC)(GLuint program, const GLchar *name);
|
||||
typedef GLboolean (KHRONOS_APIENTRY *PFNGLISENABLEDPROC)(GLenum cap);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLLINKPROGRAMPROC)(GLuint program);
|
||||
typedef void * (KHRONOS_APIENTRY *PFNGLMAPBUFFERPROC)(GLenum target, GLenum access);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLPIXELSTOREIPROC)(GLenum pname, GLint param);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLPOLYGONMODEPROC)(GLenum face, GLenum mode);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLREADPIXELSPROC)(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void *pixels);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLRENDERBUFFERSTORAGEPROC)(GLenum target, GLenum internalformat, GLsizei width, GLsizei height);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLRENDERBUFFERSTORAGEMULTISAMPLEPROC)(GLenum target, GLsizei samples, GLenum internalformat, GLsizei width, GLsizei height);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLSCISSORPROC)(GLint x, GLint y, GLsizei width, GLsizei height);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLSHADERSOURCEPROC)(GLuint shader, GLsizei count, const GLchar *const*string, const GLint *length);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLSTENCILFUNCPROC)(GLenum func, GLint ref, GLuint mask);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLSTENCILFUNCSEPARATEPROC)(GLenum face, GLenum func, GLint ref, GLuint mask);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLSTENCILMASKPROC)(GLuint mask);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLSTENCILMASKSEPARATEPROC)(GLenum face, GLuint mask);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLSTENCILOPPROC)(GLenum fail, GLenum zfail, GLenum zpass);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLSTENCILOPSEPARATEPROC)(GLenum face, GLenum sfail, GLenum dpfail, GLenum dppass);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLTEXIMAGE2DPROC)(GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLint border, GLenum format, GLenum type, const void *pixels);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLTEXPARAMETERFPROC)(GLenum target, GLenum pname, GLfloat param);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLTEXPARAMETERIPROC)(GLenum target, GLenum pname, GLint param);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLTEXSUBIMAGE2DPROC)(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLenum type, const void *pixels);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLUNIFORM1FPROC)(GLint location, GLfloat v0);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLUNIFORM1FVPROC)(GLint location, GLsizei count, const GLfloat *value);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLUNIFORM1IPROC)(GLint location, GLint v0);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLUNIFORM2FPROC)(GLint location, GLfloat v0, GLfloat v1);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLUNIFORM2FVPROC)(GLint location, GLsizei count, const GLfloat *value);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLUNIFORM3FPROC)(GLint location, GLfloat v0, GLfloat v1, GLfloat v2);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLUNIFORM4FPROC)(GLint location, GLfloat v0, GLfloat v1, GLfloat v2, GLfloat v3);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLUNIFORM4FVPROC)(GLint location, GLsizei count, const GLfloat *value);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLUNIFORMMATRIX4FVPROC)(GLint location, GLsizei count, GLboolean transpose, const GLfloat *value);
|
||||
typedef GLboolean (KHRONOS_APIENTRY *PFNGLUNMAPBUFFERPROC)(GLenum target);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLUSEPROGRAMPROC)(GLuint program);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLVERTEXATTRIBPOINTERPROC)(GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const void *pointer);
|
||||
typedef void (KHRONOS_APIENTRY *PFNGLVIEWPORTPROC)(GLint x, GLint y, GLsizei width, GLsizei height);
|
||||
|
||||
/* Global function pointers */
|
||||
GLAD_API_CALL PFNGLACTIVETEXTUREPROC glad_glActiveTexture;
|
||||
#define glActiveTexture glad_glActiveTexture
|
||||
GLAD_API_CALL PFNGLATTACHSHADERPROC glad_glAttachShader;
|
||||
#define glAttachShader glad_glAttachShader
|
||||
GLAD_API_CALL PFNGLBINDBUFFERPROC glad_glBindBuffer;
|
||||
#define glBindBuffer glad_glBindBuffer
|
||||
GLAD_API_CALL PFNGLBINDBUFFERBASEPROC glad_glBindBufferBase;
|
||||
#define glBindBufferBase glad_glBindBufferBase
|
||||
GLAD_API_CALL PFNGLBINDFRAMEBUFFERPROC glad_glBindFramebuffer;
|
||||
#define glBindFramebuffer glad_glBindFramebuffer
|
||||
GLAD_API_CALL PFNGLBINDRENDERBUFFERPROC glad_glBindRenderbuffer;
|
||||
#define glBindRenderbuffer glad_glBindRenderbuffer
|
||||
GLAD_API_CALL PFNGLBINDTEXTUREPROC glad_glBindTexture;
|
||||
#define glBindTexture glad_glBindTexture
|
||||
GLAD_API_CALL PFNGLBINDVERTEXARRAYPROC glad_glBindVertexArray;
|
||||
#define glBindVertexArray glad_glBindVertexArray
|
||||
GLAD_API_CALL PFNGLBLENDCOLORPROC glad_glBlendColor;
|
||||
#define glBlendColor glad_glBlendColor
|
||||
GLAD_API_CALL PFNGLBLENDEQUATIONPROC glad_glBlendEquation;
|
||||
#define glBlendEquation glad_glBlendEquation
|
||||
GLAD_API_CALL PFNGLBLENDEQUATIONSEPARATEPROC glad_glBlendEquationSeparate;
|
||||
#define glBlendEquationSeparate glad_glBlendEquationSeparate
|
||||
GLAD_API_CALL PFNGLBLENDFUNCPROC glad_glBlendFunc;
|
||||
#define glBlendFunc glad_glBlendFunc
|
||||
GLAD_API_CALL PFNGLBLENDFUNCSEPARATEPROC glad_glBlendFuncSeparate;
|
||||
#define glBlendFuncSeparate glad_glBlendFuncSeparate
|
||||
GLAD_API_CALL PFNGLBLITFRAMEBUFFERPROC glad_glBlitFramebuffer;
|
||||
#define glBlitFramebuffer glad_glBlitFramebuffer
|
||||
GLAD_API_CALL PFNGLBUFFERDATAPROC glad_glBufferData;
|
||||
#define glBufferData glad_glBufferData
|
||||
GLAD_API_CALL PFNGLBUFFERSUBDATAPROC glad_glBufferSubData;
|
||||
#define glBufferSubData glad_glBufferSubData
|
||||
GLAD_API_CALL PFNGLCHECKFRAMEBUFFERSTATUSPROC glad_glCheckFramebufferStatus;
|
||||
#define glCheckFramebufferStatus glad_glCheckFramebufferStatus
|
||||
GLAD_API_CALL PFNGLCLEARPROC glad_glClear;
|
||||
#define glClear glad_glClear
|
||||
GLAD_API_CALL PFNGLCLEARCOLORPROC glad_glClearColor;
|
||||
#define glClearColor glad_glClearColor
|
||||
GLAD_API_CALL PFNGLCLEARDEPTHPROC glad_glClearDepth;
|
||||
#define glClearDepth glad_glClearDepth
|
||||
GLAD_API_CALL PFNGLCLEARSTENCILPROC glad_glClearStencil;
|
||||
#define glClearStencil glad_glClearStencil
|
||||
GLAD_API_CALL PFNGLCOLORMASKPROC glad_glColorMask;
|
||||
#define glColorMask glad_glColorMask
|
||||
GLAD_API_CALL PFNGLCOMPILESHADERPROC glad_glCompileShader;
|
||||
#define glCompileShader glad_glCompileShader
|
||||
GLAD_API_CALL PFNGLCREATEPROGRAMPROC glad_glCreateProgram;
|
||||
#define glCreateProgram glad_glCreateProgram
|
||||
GLAD_API_CALL PFNGLCREATESHADERPROC glad_glCreateShader;
|
||||
#define glCreateShader glad_glCreateShader
|
||||
GLAD_API_CALL PFNGLCULLFACEPROC glad_glCullFace;
|
||||
#define glCullFace glad_glCullFace
|
||||
GLAD_API_CALL PFNGLDELETEBUFFERSPROC glad_glDeleteBuffers;
|
||||
#define glDeleteBuffers glad_glDeleteBuffers
|
||||
GLAD_API_CALL PFNGLDELETEFRAMEBUFFERSPROC glad_glDeleteFramebuffers;
|
||||
#define glDeleteFramebuffers glad_glDeleteFramebuffers
|
||||
GLAD_API_CALL PFNGLDELETEPROGRAMPROC glad_glDeleteProgram;
|
||||
#define glDeleteProgram glad_glDeleteProgram
|
||||
GLAD_API_CALL PFNGLDELETERENDERBUFFERSPROC glad_glDeleteRenderbuffers;
|
||||
#define glDeleteRenderbuffers glad_glDeleteRenderbuffers
|
||||
GLAD_API_CALL PFNGLDELETESHADERPROC glad_glDeleteShader;
|
||||
#define glDeleteShader glad_glDeleteShader
|
||||
GLAD_API_CALL PFNGLDELETETEXTURESPROC glad_glDeleteTextures;
|
||||
#define glDeleteTextures glad_glDeleteTextures
|
||||
GLAD_API_CALL PFNGLDELETEVERTEXARRAYSPROC glad_glDeleteVertexArrays;
|
||||
#define glDeleteVertexArrays glad_glDeleteVertexArrays
|
||||
GLAD_API_CALL PFNGLDEPTHFUNCPROC glad_glDepthFunc;
|
||||
#define glDepthFunc glad_glDepthFunc
|
||||
GLAD_API_CALL PFNGLDEPTHMASKPROC glad_glDepthMask;
|
||||
#define glDepthMask glad_glDepthMask
|
||||
GLAD_API_CALL PFNGLDISABLEPROC glad_glDisable;
|
||||
#define glDisable glad_glDisable
|
||||
GLAD_API_CALL PFNGLDISABLEVERTEXATTRIBARRAYPROC glad_glDisableVertexAttribArray;
|
||||
#define glDisableVertexAttribArray glad_glDisableVertexAttribArray
|
||||
GLAD_API_CALL PFNGLDRAWARRAYSPROC glad_glDrawArrays;
|
||||
#define glDrawArrays glad_glDrawArrays
|
||||
GLAD_API_CALL PFNGLDRAWBUFFERSPROC glad_glDrawBuffers;
|
||||
#define glDrawBuffers glad_glDrawBuffers
|
||||
GLAD_API_CALL PFNGLDRAWELEMENTSPROC glad_glDrawElements;
|
||||
#define glDrawElements glad_glDrawElements
|
||||
GLAD_API_CALL PFNGLENABLEPROC glad_glEnable;
|
||||
#define glEnable glad_glEnable
|
||||
GLAD_API_CALL PFNGLENABLEVERTEXATTRIBARRAYPROC glad_glEnableVertexAttribArray;
|
||||
#define glEnableVertexAttribArray glad_glEnableVertexAttribArray
|
||||
GLAD_API_CALL PFNGLFINISHPROC glad_glFinish;
|
||||
#define glFinish glad_glFinish
|
||||
GLAD_API_CALL PFNGLFLUSHPROC glad_glFlush;
|
||||
#define glFlush glad_glFlush
|
||||
GLAD_API_CALL PFNGLFRAMEBUFFERRENDERBUFFERPROC glad_glFramebufferRenderbuffer;
|
||||
#define glFramebufferRenderbuffer glad_glFramebufferRenderbuffer
|
||||
GLAD_API_CALL PFNGLFRAMEBUFFERTEXTURE2DPROC glad_glFramebufferTexture2D;
|
||||
#define glFramebufferTexture2D glad_glFramebufferTexture2D
|
||||
GLAD_API_CALL PFNGLFRONTFACEPROC glad_glFrontFace;
|
||||
#define glFrontFace glad_glFrontFace
|
||||
GLAD_API_CALL PFNGLGENBUFFERSPROC glad_glGenBuffers;
|
||||
#define glGenBuffers glad_glGenBuffers
|
||||
GLAD_API_CALL PFNGLGENFRAMEBUFFERSPROC glad_glGenFramebuffers;
|
||||
#define glGenFramebuffers glad_glGenFramebuffers
|
||||
GLAD_API_CALL PFNGLGENRENDERBUFFERSPROC glad_glGenRenderbuffers;
|
||||
#define glGenRenderbuffers glad_glGenRenderbuffers
|
||||
GLAD_API_CALL PFNGLGENTEXTURESPROC glad_glGenTextures;
|
||||
#define glGenTextures glad_glGenTextures
|
||||
GLAD_API_CALL PFNGLGENVERTEXARRAYSPROC glad_glGenVertexArrays;
|
||||
#define glGenVertexArrays glad_glGenVertexArrays
|
||||
GLAD_API_CALL PFNGLGENERATEMIPMAPPROC glad_glGenerateMipmap;
|
||||
#define glGenerateMipmap glad_glGenerateMipmap
|
||||
GLAD_API_CALL PFNGLGETATTRIBLOCATIONPROC glad_glGetAttribLocation;
|
||||
#define glGetAttribLocation glad_glGetAttribLocation
|
||||
GLAD_API_CALL PFNGLGETERRORPROC glad_glGetError;
|
||||
#define glGetError glad_glGetError
|
||||
GLAD_API_CALL PFNGLGETINTEGERVPROC glad_glGetIntegerv;
|
||||
#define glGetIntegerv glad_glGetIntegerv
|
||||
GLAD_API_CALL PFNGLGETPROGRAMINFOLOGPROC glad_glGetProgramInfoLog;
|
||||
#define glGetProgramInfoLog glad_glGetProgramInfoLog
|
||||
GLAD_API_CALL PFNGLGETPROGRAMIVPROC glad_glGetProgramiv;
|
||||
#define glGetProgramiv glad_glGetProgramiv
|
||||
GLAD_API_CALL PFNGLGETSHADERINFOLOGPROC glad_glGetShaderInfoLog;
|
||||
#define glGetShaderInfoLog glad_glGetShaderInfoLog
|
||||
GLAD_API_CALL PFNGLGETSHADERIVPROC glad_glGetShaderiv;
|
||||
#define glGetShaderiv glad_glGetShaderiv
|
||||
GLAD_API_CALL PFNGLGETSTRINGPROC glad_glGetString;
|
||||
#define glGetString glad_glGetString
|
||||
GLAD_API_CALL PFNGLGETSTRINGIPROC glad_glGetStringi;
|
||||
#define glGetStringi glad_glGetStringi
|
||||
GLAD_API_CALL PFNGLGETUNIFORMLOCATIONPROC glad_glGetUniformLocation;
|
||||
#define glGetUniformLocation glad_glGetUniformLocation
|
||||
GLAD_API_CALL PFNGLISENABLEDPROC glad_glIsEnabled;
|
||||
#define glIsEnabled glad_glIsEnabled
|
||||
GLAD_API_CALL PFNGLLINKPROGRAMPROC glad_glLinkProgram;
|
||||
#define glLinkProgram glad_glLinkProgram
|
||||
GLAD_API_CALL PFNGLMAPBUFFERPROC glad_glMapBuffer;
|
||||
#define glMapBuffer glad_glMapBuffer
|
||||
GLAD_API_CALL PFNGLPIXELSTOREIPROC glad_glPixelStorei;
|
||||
#define glPixelStorei glad_glPixelStorei
|
||||
GLAD_API_CALL PFNGLPOLYGONMODEPROC glad_glPolygonMode;
|
||||
#define glPolygonMode glad_glPolygonMode
|
||||
GLAD_API_CALL PFNGLREADPIXELSPROC glad_glReadPixels;
|
||||
#define glReadPixels glad_glReadPixels
|
||||
GLAD_API_CALL PFNGLRENDERBUFFERSTORAGEPROC glad_glRenderbufferStorage;
|
||||
#define glRenderbufferStorage glad_glRenderbufferStorage
|
||||
GLAD_API_CALL PFNGLRENDERBUFFERSTORAGEMULTISAMPLEPROC glad_glRenderbufferStorageMultisample;
|
||||
#define glRenderbufferStorageMultisample glad_glRenderbufferStorageMultisample
|
||||
GLAD_API_CALL PFNGLSCISSORPROC glad_glScissor;
|
||||
#define glScissor glad_glScissor
|
||||
GLAD_API_CALL PFNGLSHADERSOURCEPROC glad_glShaderSource;
|
||||
#define glShaderSource glad_glShaderSource
|
||||
GLAD_API_CALL PFNGLSTENCILFUNCPROC glad_glStencilFunc;
|
||||
#define glStencilFunc glad_glStencilFunc
|
||||
GLAD_API_CALL PFNGLSTENCILFUNCSEPARATEPROC glad_glStencilFuncSeparate;
|
||||
#define glStencilFuncSeparate glad_glStencilFuncSeparate
|
||||
GLAD_API_CALL PFNGLSTENCILMASKPROC glad_glStencilMask;
|
||||
#define glStencilMask glad_glStencilMask
|
||||
GLAD_API_CALL PFNGLSTENCILMASKSEPARATEPROC glad_glStencilMaskSeparate;
|
||||
#define glStencilMaskSeparate glad_glStencilMaskSeparate
|
||||
GLAD_API_CALL PFNGLSTENCILOPPROC glad_glStencilOp;
|
||||
#define glStencilOp glad_glStencilOp
|
||||
GLAD_API_CALL PFNGLSTENCILOPSEPARATEPROC glad_glStencilOpSeparate;
|
||||
#define glStencilOpSeparate glad_glStencilOpSeparate
|
||||
GLAD_API_CALL PFNGLTEXIMAGE2DPROC glad_glTexImage2D;
|
||||
#define glTexImage2D glad_glTexImage2D
|
||||
GLAD_API_CALL PFNGLTEXPARAMETERFPROC glad_glTexParameterf;
|
||||
#define glTexParameterf glad_glTexParameterf
|
||||
GLAD_API_CALL PFNGLTEXPARAMETERIPROC glad_glTexParameteri;
|
||||
#define glTexParameteri glad_glTexParameteri
|
||||
GLAD_API_CALL PFNGLTEXSUBIMAGE2DPROC glad_glTexSubImage2D;
|
||||
#define glTexSubImage2D glad_glTexSubImage2D
|
||||
GLAD_API_CALL PFNGLUNIFORM1FPROC glad_glUniform1f;
|
||||
#define glUniform1f glad_glUniform1f
|
||||
GLAD_API_CALL PFNGLUNIFORM1FVPROC glad_glUniform1fv;
|
||||
#define glUniform1fv glad_glUniform1fv
|
||||
GLAD_API_CALL PFNGLUNIFORM1IPROC glad_glUniform1i;
|
||||
#define glUniform1i glad_glUniform1i
|
||||
GLAD_API_CALL PFNGLUNIFORM2FPROC glad_glUniform2f;
|
||||
#define glUniform2f glad_glUniform2f
|
||||
GLAD_API_CALL PFNGLUNIFORM2FVPROC glad_glUniform2fv;
|
||||
#define glUniform2fv glad_glUniform2fv
|
||||
GLAD_API_CALL PFNGLUNIFORM3FPROC glad_glUniform3f;
|
||||
#define glUniform3f glad_glUniform3f
|
||||
GLAD_API_CALL PFNGLUNIFORM4FPROC glad_glUniform4f;
|
||||
#define glUniform4f glad_glUniform4f
|
||||
GLAD_API_CALL PFNGLUNIFORM4FVPROC glad_glUniform4fv;
|
||||
#define glUniform4fv glad_glUniform4fv
|
||||
GLAD_API_CALL PFNGLUNIFORMMATRIX4FVPROC glad_glUniformMatrix4fv;
|
||||
#define glUniformMatrix4fv glad_glUniformMatrix4fv
|
||||
GLAD_API_CALL PFNGLUNMAPBUFFERPROC glad_glUnmapBuffer;
|
||||
#define glUnmapBuffer glad_glUnmapBuffer
|
||||
GLAD_API_CALL PFNGLUSEPROGRAMPROC glad_glUseProgram;
|
||||
#define glUseProgram glad_glUseProgram
|
||||
GLAD_API_CALL PFNGLVERTEXATTRIBPOINTERPROC glad_glVertexAttribPointer;
|
||||
#define glVertexAttribPointer glad_glVertexAttribPointer
|
||||
GLAD_API_CALL PFNGLVIEWPORTPROC glad_glViewport;
|
||||
#define glViewport glad_glViewport
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* GLAD_GL_H_ */
|
||||
322
designer/glad/src/gl.c
Normal file
322
designer/glad/src/gl.c
Normal file
@@ -0,0 +1,322 @@
|
||||
/*
|
||||
* GLAD OpenGL 3.3 Core Profile Loader Implementation
|
||||
*/
|
||||
|
||||
#include <glad/gl.h>
|
||||
#include <string.h>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
static HMODULE libGL;
|
||||
|
||||
static int open_gl(void) {
|
||||
libGL = LoadLibraryW(L"opengl32.dll");
|
||||
return libGL != NULL;
|
||||
}
|
||||
|
||||
static void close_gl(void) {
|
||||
if (libGL != NULL) {
|
||||
FreeLibrary(libGL);
|
||||
libGL = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
static GLADapiproc get_proc(const char *name) {
|
||||
GLADapiproc result = NULL;
|
||||
|
||||
/* Try wglGetProcAddress first for extension functions */
|
||||
typedef GLADapiproc (KHRONOS_APIENTRY *PFNWGLGETPROCADDRESSPROC)(const char*);
|
||||
static PFNWGLGETPROCADDRESSPROC wglGetProcAddress_ptr = NULL;
|
||||
|
||||
if (wglGetProcAddress_ptr == NULL && libGL != NULL) {
|
||||
wglGetProcAddress_ptr = (PFNWGLGETPROCADDRESSPROC)GetProcAddress(libGL, "wglGetProcAddress");
|
||||
}
|
||||
|
||||
if (wglGetProcAddress_ptr != NULL) {
|
||||
result = wglGetProcAddress_ptr(name);
|
||||
}
|
||||
|
||||
/* Fall back to GetProcAddress for core functions */
|
||||
if (result == NULL && libGL != NULL) {
|
||||
result = (GLADapiproc)GetProcAddress(libGL, name);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
#elif defined(__APPLE__)
|
||||
#include <dlfcn.h>
|
||||
static void* libGL;
|
||||
|
||||
static int open_gl(void) {
|
||||
libGL = dlopen("/System/Library/Frameworks/OpenGL.framework/OpenGL", RTLD_LAZY | RTLD_LOCAL);
|
||||
return libGL != NULL;
|
||||
}
|
||||
|
||||
static void close_gl(void) {
|
||||
if (libGL != NULL) {
|
||||
dlclose(libGL);
|
||||
libGL = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
static GLADapiproc get_proc(const char *name) {
|
||||
if (libGL == NULL) return NULL;
|
||||
return (GLADapiproc)dlsym(libGL, name);
|
||||
}
|
||||
|
||||
#else /* Linux/Unix */
|
||||
#include <dlfcn.h>
|
||||
static void* libGL;
|
||||
|
||||
static int open_gl(void) {
|
||||
libGL = dlopen("libGL.so.1", RTLD_LAZY | RTLD_LOCAL);
|
||||
if (libGL == NULL) {
|
||||
libGL = dlopen("libGL.so", RTLD_LAZY | RTLD_LOCAL);
|
||||
}
|
||||
return libGL != NULL;
|
||||
}
|
||||
|
||||
static void close_gl(void) {
|
||||
if (libGL != NULL) {
|
||||
dlclose(libGL);
|
||||
libGL = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
static GLADapiproc get_proc(const char *name) {
|
||||
if (libGL == NULL) return NULL;
|
||||
return (GLADapiproc)dlsym(libGL, name);
|
||||
}
|
||||
#endif
|
||||
|
||||
/* Function pointers */
|
||||
PFNGLACTIVETEXTUREPROC glad_glActiveTexture = NULL;
|
||||
PFNGLATTACHSHADERPROC glad_glAttachShader = NULL;
|
||||
PFNGLBINDBUFFERPROC glad_glBindBuffer = NULL;
|
||||
PFNGLBINDBUFFERBASEPROC glad_glBindBufferBase = NULL;
|
||||
PFNGLBINDFRAMEBUFFERPROC glad_glBindFramebuffer = NULL;
|
||||
PFNGLBINDRENDERBUFFERPROC glad_glBindRenderbuffer = NULL;
|
||||
PFNGLBINDTEXTUREPROC glad_glBindTexture = NULL;
|
||||
PFNGLBINDVERTEXARRAYPROC glad_glBindVertexArray = NULL;
|
||||
PFNGLBLENDCOLORPROC glad_glBlendColor = NULL;
|
||||
PFNGLBLENDEQUATIONPROC glad_glBlendEquation = NULL;
|
||||
PFNGLBLENDEQUATIONSEPARATEPROC glad_glBlendEquationSeparate = NULL;
|
||||
PFNGLBLENDFUNCPROC glad_glBlendFunc = NULL;
|
||||
PFNGLBLENDFUNCSEPARATEPROC glad_glBlendFuncSeparate = NULL;
|
||||
PFNGLBLITFRAMEBUFFERPROC glad_glBlitFramebuffer = NULL;
|
||||
PFNGLBUFFERDATAPROC glad_glBufferData = NULL;
|
||||
PFNGLBUFFERSUBDATAPROC glad_glBufferSubData = NULL;
|
||||
PFNGLCHECKFRAMEBUFFERSTATUSPROC glad_glCheckFramebufferStatus = NULL;
|
||||
PFNGLCLEARPROC glad_glClear = NULL;
|
||||
PFNGLCLEARCOLORPROC glad_glClearColor = NULL;
|
||||
PFNGLCLEARDEPTHPROC glad_glClearDepth = NULL;
|
||||
PFNGLCLEARSTENCILPROC glad_glClearStencil = NULL;
|
||||
PFNGLCOLORMASKPROC glad_glColorMask = NULL;
|
||||
PFNGLCOMPILESHADERPROC glad_glCompileShader = NULL;
|
||||
PFNGLCREATEPROGRAMPROC glad_glCreateProgram = NULL;
|
||||
PFNGLCREATESHADERPROC glad_glCreateShader = NULL;
|
||||
PFNGLCULLFACEPROC glad_glCullFace = NULL;
|
||||
PFNGLDELETEBUFFERSPROC glad_glDeleteBuffers = NULL;
|
||||
PFNGLDELETEFRAMEBUFFERSPROC glad_glDeleteFramebuffers = NULL;
|
||||
PFNGLDELETEPROGRAMPROC glad_glDeleteProgram = NULL;
|
||||
PFNGLDELETERENDERBUFFERSPROC glad_glDeleteRenderbuffers = NULL;
|
||||
PFNGLDELETESHADERPROC glad_glDeleteShader = NULL;
|
||||
PFNGLDELETETEXTURESPROC glad_glDeleteTextures = NULL;
|
||||
PFNGLDELETEVERTEXARRAYSPROC glad_glDeleteVertexArrays = NULL;
|
||||
PFNGLDEPTHFUNCPROC glad_glDepthFunc = NULL;
|
||||
PFNGLDEPTHMASKPROC glad_glDepthMask = NULL;
|
||||
PFNGLDISABLEPROC glad_glDisable = NULL;
|
||||
PFNGLDISABLEVERTEXATTRIBARRAYPROC glad_glDisableVertexAttribArray = NULL;
|
||||
PFNGLDRAWARRAYSPROC glad_glDrawArrays = NULL;
|
||||
PFNGLDRAWBUFFERSPROC glad_glDrawBuffers = NULL;
|
||||
PFNGLDRAWELEMENTSPROC glad_glDrawElements = NULL;
|
||||
PFNGLENABLEPROC glad_glEnable = NULL;
|
||||
PFNGLENABLEVERTEXATTRIBARRAYPROC glad_glEnableVertexAttribArray = NULL;
|
||||
PFNGLFINISHPROC glad_glFinish = NULL;
|
||||
PFNGLFLUSHPROC glad_glFlush = NULL;
|
||||
PFNGLFRAMEBUFFERRENDERBUFFERPROC glad_glFramebufferRenderbuffer = NULL;
|
||||
PFNGLFRAMEBUFFERTEXTURE2DPROC glad_glFramebufferTexture2D = NULL;
|
||||
PFNGLFRONTFACEPROC glad_glFrontFace = NULL;
|
||||
PFNGLGENBUFFERSPROC glad_glGenBuffers = NULL;
|
||||
PFNGLGENFRAMEBUFFERSPROC glad_glGenFramebuffers = NULL;
|
||||
PFNGLGENRENDERBUFFERSPROC glad_glGenRenderbuffers = NULL;
|
||||
PFNGLGENTEXTURESPROC glad_glGenTextures = NULL;
|
||||
PFNGLGENVERTEXARRAYSPROC glad_glGenVertexArrays = NULL;
|
||||
PFNGLGENERATEMIPMAPPROC glad_glGenerateMipmap = NULL;
|
||||
PFNGLGETATTRIBLOCATIONPROC glad_glGetAttribLocation = NULL;
|
||||
PFNGLGETERRORPROC glad_glGetError = NULL;
|
||||
PFNGLGETINTEGERVPROC glad_glGetIntegerv = NULL;
|
||||
PFNGLGETPROGRAMINFOLOGPROC glad_glGetProgramInfoLog = NULL;
|
||||
PFNGLGETPROGRAMIVPROC glad_glGetProgramiv = NULL;
|
||||
PFNGLGETSHADERINFOLOGPROC glad_glGetShaderInfoLog = NULL;
|
||||
PFNGLGETSHADERIVPROC glad_glGetShaderiv = NULL;
|
||||
PFNGLGETSTRINGPROC glad_glGetString = NULL;
|
||||
PFNGLGETSTRINGIPROC glad_glGetStringi = NULL;
|
||||
PFNGLGETUNIFORMLOCATIONPROC glad_glGetUniformLocation = NULL;
|
||||
PFNGLISENABLEDPROC glad_glIsEnabled = NULL;
|
||||
PFNGLLINKPROGRAMPROC glad_glLinkProgram = NULL;
|
||||
PFNGLMAPBUFFERPROC glad_glMapBuffer = NULL;
|
||||
PFNGLPIXELSTOREIPROC glad_glPixelStorei = NULL;
|
||||
PFNGLPOLYGONMODEPROC glad_glPolygonMode = NULL;
|
||||
PFNGLREADPIXELSPROC glad_glReadPixels = NULL;
|
||||
PFNGLRENDERBUFFERSTORAGEPROC glad_glRenderbufferStorage = NULL;
|
||||
PFNGLRENDERBUFFERSTORAGEMULTISAMPLEPROC glad_glRenderbufferStorageMultisample = NULL;
|
||||
PFNGLSCISSORPROC glad_glScissor = NULL;
|
||||
PFNGLSHADERSOURCEPROC glad_glShaderSource = NULL;
|
||||
PFNGLSTENCILFUNCPROC glad_glStencilFunc = NULL;
|
||||
PFNGLSTENCILFUNCSEPARATEPROC glad_glStencilFuncSeparate = NULL;
|
||||
PFNGLSTENCILMASKPROC glad_glStencilMask = NULL;
|
||||
PFNGLSTENCILMASKSEPARATEPROC glad_glStencilMaskSeparate = NULL;
|
||||
PFNGLSTENCILOPPROC glad_glStencilOp = NULL;
|
||||
PFNGLSTENCILOPSEPARATEPROC glad_glStencilOpSeparate = NULL;
|
||||
PFNGLTEXIMAGE2DPROC glad_glTexImage2D = NULL;
|
||||
PFNGLTEXPARAMETERFPROC glad_glTexParameterf = NULL;
|
||||
PFNGLTEXPARAMETERIPROC glad_glTexParameteri = NULL;
|
||||
PFNGLTEXSUBIMAGE2DPROC glad_glTexSubImage2D = NULL;
|
||||
PFNGLUNIFORM1FPROC glad_glUniform1f = NULL;
|
||||
PFNGLUNIFORM1FVPROC glad_glUniform1fv = NULL;
|
||||
PFNGLUNIFORM1IPROC glad_glUniform1i = NULL;
|
||||
PFNGLUNIFORM2FPROC glad_glUniform2f = NULL;
|
||||
PFNGLUNIFORM2FVPROC glad_glUniform2fv = NULL;
|
||||
PFNGLUNIFORM3FPROC glad_glUniform3f = NULL;
|
||||
PFNGLUNIFORM4FPROC glad_glUniform4f = NULL;
|
||||
PFNGLUNIFORM4FVPROC glad_glUniform4fv = NULL;
|
||||
PFNGLUNIFORMMATRIX4FVPROC glad_glUniformMatrix4fv = NULL;
|
||||
PFNGLUNMAPBUFFERPROC glad_glUnmapBuffer = NULL;
|
||||
PFNGLUSEPROGRAMPROC glad_glUseProgram = NULL;
|
||||
PFNGLVERTEXATTRIBPOINTERPROC glad_glVertexAttribPointer = NULL;
|
||||
PFNGLVIEWPORTPROC glad_glViewport = NULL;
|
||||
|
||||
static void load_GL_VERSION_1_0(GLADloadfunc load) {
|
||||
glad_glClear = (PFNGLCLEARPROC)load("glClear");
|
||||
glad_glClearColor = (PFNGLCLEARCOLORPROC)load("glClearColor");
|
||||
glad_glClearDepth = (PFNGLCLEARDEPTHPROC)load("glClearDepth");
|
||||
glad_glClearStencil = (PFNGLCLEARSTENCILPROC)load("glClearStencil");
|
||||
glad_glColorMask = (PFNGLCOLORMASKPROC)load("glColorMask");
|
||||
glad_glCullFace = (PFNGLCULLFACEPROC)load("glCullFace");
|
||||
glad_glDepthFunc = (PFNGLDEPTHFUNCPROC)load("glDepthFunc");
|
||||
glad_glDepthMask = (PFNGLDEPTHMASKPROC)load("glDepthMask");
|
||||
glad_glDisable = (PFNGLDISABLEPROC)load("glDisable");
|
||||
glad_glDrawArrays = (PFNGLDRAWARRAYSPROC)load("glDrawArrays");
|
||||
glad_glDrawElements = (PFNGLDRAWELEMENTSPROC)load("glDrawElements");
|
||||
glad_glEnable = (PFNGLENABLEPROC)load("glEnable");
|
||||
glad_glFinish = (PFNGLFINISHPROC)load("glFinish");
|
||||
glad_glFlush = (PFNGLFLUSHPROC)load("glFlush");
|
||||
glad_glFrontFace = (PFNGLFRONTFACEPROC)load("glFrontFace");
|
||||
glad_glGetError = (PFNGLGETERRORPROC)load("glGetError");
|
||||
glad_glGetIntegerv = (PFNGLGETINTEGERVPROC)load("glGetIntegerv");
|
||||
glad_glGetString = (PFNGLGETSTRINGPROC)load("glGetString");
|
||||
glad_glIsEnabled = (PFNGLISENABLEDPROC)load("glIsEnabled");
|
||||
glad_glPixelStorei = (PFNGLPIXELSTOREIPROC)load("glPixelStorei");
|
||||
glad_glPolygonMode = (PFNGLPOLYGONMODEPROC)load("glPolygonMode");
|
||||
glad_glReadPixels = (PFNGLREADPIXELSPROC)load("glReadPixels");
|
||||
glad_glScissor = (PFNGLSCISSORPROC)load("glScissor");
|
||||
glad_glStencilFunc = (PFNGLSTENCILFUNCPROC)load("glStencilFunc");
|
||||
glad_glStencilMask = (PFNGLSTENCILMASKPROC)load("glStencilMask");
|
||||
glad_glStencilOp = (PFNGLSTENCILOPPROC)load("glStencilOp");
|
||||
glad_glTexImage2D = (PFNGLTEXIMAGE2DPROC)load("glTexImage2D");
|
||||
glad_glTexParameterf = (PFNGLTEXPARAMETERFPROC)load("glTexParameterf");
|
||||
glad_glTexParameteri = (PFNGLTEXPARAMETERIPROC)load("glTexParameteri");
|
||||
glad_glViewport = (PFNGLVIEWPORTPROC)load("glViewport");
|
||||
}
|
||||
|
||||
static void load_GL_VERSION_1_1(GLADloadfunc load) {
|
||||
glad_glBindTexture = (PFNGLBINDTEXTUREPROC)load("glBindTexture");
|
||||
glad_glDeleteTextures = (PFNGLDELETETEXTURESPROC)load("glDeleteTextures");
|
||||
glad_glGenTextures = (PFNGLGENTEXTURESPROC)load("glGenTextures");
|
||||
glad_glTexSubImage2D = (PFNGLTEXSUBIMAGE2DPROC)load("glTexSubImage2D");
|
||||
}
|
||||
|
||||
static void load_GL_VERSION_1_3(GLADloadfunc load) {
|
||||
glad_glActiveTexture = (PFNGLACTIVETEXTUREPROC)load("glActiveTexture");
|
||||
}
|
||||
|
||||
static void load_GL_VERSION_1_4(GLADloadfunc load) {
|
||||
glad_glBlendColor = (PFNGLBLENDCOLORPROC)load("glBlendColor");
|
||||
glad_glBlendEquation = (PFNGLBLENDEQUATIONPROC)load("glBlendEquation");
|
||||
glad_glBlendFuncSeparate = (PFNGLBLENDFUNCSEPARATEPROC)load("glBlendFuncSeparate");
|
||||
}
|
||||
|
||||
static void load_GL_VERSION_1_5(GLADloadfunc load) {
|
||||
glad_glBindBuffer = (PFNGLBINDBUFFERPROC)load("glBindBuffer");
|
||||
glad_glBufferData = (PFNGLBUFFERDATAPROC)load("glBufferData");
|
||||
glad_glBufferSubData = (PFNGLBUFFERSUBDATAPROC)load("glBufferSubData");
|
||||
glad_glDeleteBuffers = (PFNGLDELETEBUFFERSPROC)load("glDeleteBuffers");
|
||||
glad_glGenBuffers = (PFNGLGENBUFFERSPROC)load("glGenBuffers");
|
||||
glad_glMapBuffer = (PFNGLMAPBUFFERPROC)load("glMapBuffer");
|
||||
glad_glUnmapBuffer = (PFNGLUNMAPBUFFERPROC)load("glUnmapBuffer");
|
||||
}
|
||||
|
||||
static void load_GL_VERSION_2_0(GLADloadfunc load) {
|
||||
glad_glAttachShader = (PFNGLATTACHSHADERPROC)load("glAttachShader");
|
||||
glad_glBlendEquationSeparate = (PFNGLBLENDEQUATIONSEPARATEPROC)load("glBlendEquationSeparate");
|
||||
glad_glBlendFunc = (PFNGLBLENDFUNCPROC)load("glBlendFunc");
|
||||
glad_glCompileShader = (PFNGLCOMPILESHADERPROC)load("glCompileShader");
|
||||
glad_glCreateProgram = (PFNGLCREATEPROGRAMPROC)load("glCreateProgram");
|
||||
glad_glCreateShader = (PFNGLCREATESHADERPROC)load("glCreateShader");
|
||||
glad_glDeleteProgram = (PFNGLDELETEPROGRAMPROC)load("glDeleteProgram");
|
||||
glad_glDeleteShader = (PFNGLDELETESHADERPROC)load("glDeleteShader");
|
||||
glad_glDisableVertexAttribArray = (PFNGLDISABLEVERTEXATTRIBARRAYPROC)load("glDisableVertexAttribArray");
|
||||
glad_glDrawBuffers = (PFNGLDRAWBUFFERSPROC)load("glDrawBuffers");
|
||||
glad_glEnableVertexAttribArray = (PFNGLENABLEVERTEXATTRIBARRAYPROC)load("glEnableVertexAttribArray");
|
||||
glad_glGetAttribLocation = (PFNGLGETATTRIBLOCATIONPROC)load("glGetAttribLocation");
|
||||
glad_glGetProgramInfoLog = (PFNGLGETPROGRAMINFOLOGPROC)load("glGetProgramInfoLog");
|
||||
glad_glGetProgramiv = (PFNGLGETPROGRAMIVPROC)load("glGetProgramiv");
|
||||
glad_glGetShaderInfoLog = (PFNGLGETSHADERINFOLOGPROC)load("glGetShaderInfoLog");
|
||||
glad_glGetShaderiv = (PFNGLGETSHADERIVPROC)load("glGetShaderiv");
|
||||
glad_glGetUniformLocation = (PFNGLGETUNIFORMLOCATIONPROC)load("glGetUniformLocation");
|
||||
glad_glLinkProgram = (PFNGLLINKPROGRAMPROC)load("glLinkProgram");
|
||||
glad_glShaderSource = (PFNGLSHADERSOURCEPROC)load("glShaderSource");
|
||||
glad_glStencilFuncSeparate = (PFNGLSTENCILFUNCSEPARATEPROC)load("glStencilFuncSeparate");
|
||||
glad_glStencilMaskSeparate = (PFNGLSTENCILMASKSEPARATEPROC)load("glStencilMaskSeparate");
|
||||
glad_glStencilOpSeparate = (PFNGLSTENCILOPSEPARATEPROC)load("glStencilOpSeparate");
|
||||
glad_glUniform1f = (PFNGLUNIFORM1FPROC)load("glUniform1f");
|
||||
glad_glUniform1fv = (PFNGLUNIFORM1FVPROC)load("glUniform1fv");
|
||||
glad_glUniform1i = (PFNGLUNIFORM1IPROC)load("glUniform1i");
|
||||
glad_glUniform2f = (PFNGLUNIFORM2FPROC)load("glUniform2f");
|
||||
glad_glUniform2fv = (PFNGLUNIFORM2FVPROC)load("glUniform2fv");
|
||||
glad_glUniform3f = (PFNGLUNIFORM3FPROC)load("glUniform3f");
|
||||
glad_glUniform4f = (PFNGLUNIFORM4FPROC)load("glUniform4f");
|
||||
glad_glUniform4fv = (PFNGLUNIFORM4FVPROC)load("glUniform4fv");
|
||||
glad_glUniformMatrix4fv = (PFNGLUNIFORMMATRIX4FVPROC)load("glUniformMatrix4fv");
|
||||
glad_glUseProgram = (PFNGLUSEPROGRAMPROC)load("glUseProgram");
|
||||
glad_glVertexAttribPointer = (PFNGLVERTEXATTRIBPOINTERPROC)load("glVertexAttribPointer");
|
||||
}
|
||||
|
||||
static void load_GL_VERSION_3_0(GLADloadfunc load) {
|
||||
glad_glBindBufferBase = (PFNGLBINDBUFFERBASEPROC)load("glBindBufferBase");
|
||||
glad_glBindFramebuffer = (PFNGLBINDFRAMEBUFFERPROC)load("glBindFramebuffer");
|
||||
glad_glBindRenderbuffer = (PFNGLBINDRENDERBUFFERPROC)load("glBindRenderbuffer");
|
||||
glad_glBindVertexArray = (PFNGLBINDVERTEXARRAYPROC)load("glBindVertexArray");
|
||||
glad_glBlitFramebuffer = (PFNGLBLITFRAMEBUFFERPROC)load("glBlitFramebuffer");
|
||||
glad_glCheckFramebufferStatus = (PFNGLCHECKFRAMEBUFFERSTATUSPROC)load("glCheckFramebufferStatus");
|
||||
glad_glDeleteFramebuffers = (PFNGLDELETEFRAMEBUFFERSPROC)load("glDeleteFramebuffers");
|
||||
glad_glDeleteRenderbuffers = (PFNGLDELETERENDERBUFFERSPROC)load("glDeleteRenderbuffers");
|
||||
glad_glDeleteVertexArrays = (PFNGLDELETEVERTEXARRAYSPROC)load("glDeleteVertexArrays");
|
||||
glad_glFramebufferRenderbuffer = (PFNGLFRAMEBUFFERRENDERBUFFERPROC)load("glFramebufferRenderbuffer");
|
||||
glad_glFramebufferTexture2D = (PFNGLFRAMEBUFFERTEXTURE2DPROC)load("glFramebufferTexture2D");
|
||||
glad_glGenFramebuffers = (PFNGLGENFRAMEBUFFERSPROC)load("glGenFramebuffers");
|
||||
glad_glGenRenderbuffers = (PFNGLGENRENDERBUFFERSPROC)load("glGenRenderbuffers");
|
||||
glad_glGenVertexArrays = (PFNGLGENVERTEXARRAYSPROC)load("glGenVertexArrays");
|
||||
glad_glGenerateMipmap = (PFNGLGENERATEMIPMAPPROC)load("glGenerateMipmap");
|
||||
glad_glGetStringi = (PFNGLGETSTRINGIPROC)load("glGetStringi");
|
||||
glad_glRenderbufferStorage = (PFNGLRENDERBUFFERSTORAGEPROC)load("glRenderbufferStorage");
|
||||
glad_glRenderbufferStorageMultisample = (PFNGLRENDERBUFFERSTORAGEMULTISAMPLEPROC)load("glRenderbufferStorageMultisample");
|
||||
}
|
||||
|
||||
int gladLoadGL(GLADloadfunc load) {
|
||||
load_GL_VERSION_1_0(load);
|
||||
load_GL_VERSION_1_1(load);
|
||||
load_GL_VERSION_1_3(load);
|
||||
load_GL_VERSION_1_4(load);
|
||||
load_GL_VERSION_1_5(load);
|
||||
load_GL_VERSION_2_0(load);
|
||||
load_GL_VERSION_3_0(load);
|
||||
return GLAD_MAKE_VERSION(3, 3);
|
||||
}
|
||||
|
||||
int gladLoadGLUserPtr(GLADloadfunc load, void *userptr) {
|
||||
(void)userptr;
|
||||
return gladLoadGL(load);
|
||||
}
|
||||
276
designer/src/data_models.cpp
Normal file
276
designer/src/data_models.cpp
Normal file
@@ -0,0 +1,276 @@
|
||||
// Data models implementation
|
||||
#include "data_models.h"
|
||||
#include <iostream>
|
||||
|
||||
// Global data instances
|
||||
SettingsData g_settings;
|
||||
PhoneData g_phone;
|
||||
BrowserData g_browser;
|
||||
std::vector<Conversation> g_conversations;
|
||||
std::vector<Contact> g_contacts;
|
||||
int g_selected_conversation = -1;
|
||||
int g_selected_contact = -1;
|
||||
|
||||
// Data model handles
|
||||
Rml::DataModelHandle g_settings_model;
|
||||
Rml::DataModelHandle g_phone_model;
|
||||
Rml::DataModelHandle g_messages_model;
|
||||
Rml::DataModelHandle g_contacts_model;
|
||||
Rml::DataModelHandle g_browser_model;
|
||||
|
||||
void initializeSampleData()
|
||||
{
|
||||
// Initialize contacts
|
||||
g_contacts = {
|
||||
{1, "Alice Johnson", "+1 (555) 123-4567", "alice@example.com", "#E91E63", "A"},
|
||||
{2, "Andrew Smith", "+1 (555) 234-5678", "andrew@example.com", "#9C27B0", "A"},
|
||||
{3, "Bob Williams", "+1 (555) 345-6789", "bob@example.com", "#2196F3", "B"},
|
||||
{4, "Brian Davis", "+1 (555) 456-7890", "brian@example.com", "#00BCD4", "B"},
|
||||
{5, "Carol Martinez", "+1 (555) 567-8901", "carol@example.com", "#4CAF50", "C"},
|
||||
{6, "David Lee", "+1 (555) 678-9012", "david@example.com", "#FF9800", "D"},
|
||||
{7, "John Wilson", "+1 (555) 789-0123", "john@example.com", "#F44336", "J"},
|
||||
{8, "Mom", "+1 (555) 890-1234", "mom@example.com", "#673AB7", "M"},
|
||||
{9, "Mike Brown", "+1 (555) 901-2345", "mike@example.com", "#3F51B5", "M"},
|
||||
{10, "Sarah Taylor", "+1 (555) 012-3456", "sarah@example.com", "#009688", "S"}
|
||||
};
|
||||
|
||||
// Initialize conversations
|
||||
g_conversations = {
|
||||
{1, "John Wilson", "#4CAF50", "Hey, are you coming to the party tonight?", "2:30 PM", 2, {
|
||||
{"them", "Hey!", "2:25 PM"},
|
||||
{"them", "What are you up to?", "2:26 PM"},
|
||||
{"me", "Not much, just working", "2:27 PM"},
|
||||
{"them", "Cool! There's a party at Mike's tonight", "2:28 PM"},
|
||||
{"them", "Hey, are you coming to the party tonight?", "2:30 PM"}
|
||||
}},
|
||||
{2, "Mom", "#673AB7", "Don't forget to call your grandmother!", "1:15 PM", 0, {
|
||||
{"them", "Hi sweetie!", "1:00 PM"},
|
||||
{"me", "Hi Mom!", "1:05 PM"},
|
||||
{"them", "How are you doing?", "1:10 PM"},
|
||||
{"me", "I'm good, how are you?", "1:12 PM"},
|
||||
{"them", "Don't forget to call your grandmother!", "1:15 PM"}
|
||||
}},
|
||||
{3, "Alice Johnson", "#E91E63", "Thanks for the help with the project!", "Yesterday", 0, {
|
||||
{"me", "Here's the file you needed", "Yesterday"},
|
||||
{"them", "Thanks for the help with the project!", "Yesterday"}
|
||||
}},
|
||||
{4, "Bob Williams", "#2196F3", "Did you see the game last night?", "Yesterday", 0, {
|
||||
{"them", "Did you see the game last night?", "Yesterday"}
|
||||
}},
|
||||
{5, "Work Group", "#FF9800", "Sarah: Meeting moved to 3pm", "Mon", 0, {
|
||||
{"Sarah", "Meeting moved to 3pm", "Mon"}
|
||||
}},
|
||||
{6, "Sarah Taylor", "#009688", "See you at the coffee shop!", "Sun", 0, {
|
||||
{"them", "See you at the coffee shop!", "Sun"}
|
||||
}},
|
||||
{7, "David Lee", "#F44336", "Great talking to you!", "Sat", 0, {
|
||||
{"them", "Great talking to you!", "Sat"}
|
||||
}}
|
||||
};
|
||||
|
||||
// Initialize browser tabs
|
||||
g_browser.tabs = {
|
||||
{"example.com", "Example Domain", false},
|
||||
{"rmlui.github.io", "RmlUi Documentation", false},
|
||||
{"github.com", "GitHub", false}
|
||||
};
|
||||
|
||||
std::cout << "Sample data initialized: " << g_contacts.size() << " contacts, "
|
||||
<< g_conversations.size() << " conversations, "
|
||||
<< g_browser.tabs.size() << " browser tabs" << std::endl;
|
||||
}
|
||||
|
||||
void setupDataModels(Rml::Context* context)
|
||||
{
|
||||
// Messages data model
|
||||
if (auto constructor = context->CreateDataModel("messages"))
|
||||
{
|
||||
if (auto msg_handle = constructor.RegisterStruct<Message>())
|
||||
{
|
||||
msg_handle.RegisterMember("from", &Message::from);
|
||||
msg_handle.RegisterMember("text", &Message::text);
|
||||
msg_handle.RegisterMember("time", &Message::time);
|
||||
}
|
||||
|
||||
if (auto conv_handle = constructor.RegisterStruct<Conversation>())
|
||||
{
|
||||
conv_handle.RegisterMember("id", &Conversation::id);
|
||||
conv_handle.RegisterMember("name", &Conversation::name);
|
||||
conv_handle.RegisterMember("color", &Conversation::color);
|
||||
conv_handle.RegisterMember("last_message", &Conversation::last_message);
|
||||
conv_handle.RegisterMember("time", &Conversation::time);
|
||||
conv_handle.RegisterMember("unread", &Conversation::unread);
|
||||
conv_handle.RegisterMember("messages", &Conversation::messages);
|
||||
}
|
||||
|
||||
constructor.RegisterArray<std::vector<Message>>();
|
||||
constructor.RegisterArray<std::vector<Conversation>>();
|
||||
|
||||
constructor.Bind("conversations", &g_conversations);
|
||||
constructor.Bind("selected_conversation", &g_selected_conversation);
|
||||
|
||||
constructor.BindEventCallback("select_conversation",
|
||||
[](Rml::DataModelHandle handle, Rml::Event& event, const Rml::VariantList& args) {
|
||||
if (!args.empty()) {
|
||||
g_selected_conversation = args[0].Get<int>();
|
||||
std::cout << "Selected conversation: " << g_selected_conversation << std::endl;
|
||||
handle.DirtyVariable("selected_conversation");
|
||||
}
|
||||
});
|
||||
|
||||
g_messages_model = constructor.GetModelHandle();
|
||||
std::cout << "Messages data model created" << std::endl;
|
||||
}
|
||||
|
||||
// Contacts data model
|
||||
if (auto constructor = context->CreateDataModel("contacts"))
|
||||
{
|
||||
if (auto contact_handle = constructor.RegisterStruct<Contact>())
|
||||
{
|
||||
contact_handle.RegisterMember("id", &Contact::id);
|
||||
contact_handle.RegisterMember("name", &Contact::name);
|
||||
contact_handle.RegisterMember("phone", &Contact::phone);
|
||||
contact_handle.RegisterMember("email", &Contact::email);
|
||||
contact_handle.RegisterMember("color", &Contact::color);
|
||||
contact_handle.RegisterMember("initial", &Contact::initial);
|
||||
}
|
||||
|
||||
constructor.RegisterArray<std::vector<Contact>>();
|
||||
|
||||
constructor.Bind("contacts", &g_contacts);
|
||||
constructor.Bind("selected_contact", &g_selected_contact);
|
||||
|
||||
constructor.BindEventCallback("select_contact",
|
||||
[](Rml::DataModelHandle handle, Rml::Event& event, const Rml::VariantList& args) {
|
||||
if (!args.empty()) {
|
||||
g_selected_contact = args[0].Get<int>();
|
||||
std::cout << "Selected contact: " << g_selected_contact << std::endl;
|
||||
handle.DirtyVariable("selected_contact");
|
||||
}
|
||||
});
|
||||
|
||||
g_contacts_model = constructor.GetModelHandle();
|
||||
std::cout << "Contacts data model created" << std::endl;
|
||||
}
|
||||
|
||||
// Settings data model
|
||||
if (auto constructor = context->CreateDataModel("settings"))
|
||||
{
|
||||
constructor.Bind("wifi", &g_settings.wifi);
|
||||
constructor.Bind("bluetooth", &g_settings.bluetooth);
|
||||
constructor.Bind("airplane_mode", &g_settings.airplane_mode);
|
||||
constructor.Bind("location", &g_settings.location);
|
||||
constructor.Bind("dark_mode", &g_settings.dark_mode);
|
||||
constructor.Bind("notifications", &g_settings.notifications);
|
||||
constructor.Bind("do_not_disturb", &g_settings.do_not_disturb);
|
||||
constructor.Bind("user_name", &g_settings.user_name);
|
||||
constructor.Bind("user_email", &g_settings.user_email);
|
||||
constructor.Bind("wifi_network", &g_settings.wifi_network);
|
||||
constructor.Bind("battery_percent", &g_settings.battery_percent);
|
||||
constructor.Bind("battery_remaining", &g_settings.battery_remaining);
|
||||
constructor.Bind("storage_used", &g_settings.storage_used);
|
||||
|
||||
g_settings_model = constructor.GetModelHandle();
|
||||
std::cout << "Settings data model created" << std::endl;
|
||||
}
|
||||
|
||||
// Phone data model
|
||||
if (auto constructor = context->CreateDataModel("phone"))
|
||||
{
|
||||
constructor.Bind("dial_number", &g_phone.dial_number);
|
||||
constructor.Bind("is_calling", &g_phone.is_calling);
|
||||
constructor.Bind("call_contact", &g_phone.call_contact);
|
||||
constructor.Bind("call_duration", &g_phone.call_duration);
|
||||
|
||||
constructor.BindEventCallback("dial_press",
|
||||
[](Rml::DataModelHandle handle, Rml::Event& event, const Rml::VariantList& args) {
|
||||
if (!args.empty()) {
|
||||
g_phone.dial_number += args[0].Get<Rml::String>();
|
||||
std::cout << "Dial: " << g_phone.dial_number << std::endl;
|
||||
handle.DirtyVariable("dial_number");
|
||||
}
|
||||
});
|
||||
|
||||
constructor.BindEventCallback("dial_backspace",
|
||||
[](Rml::DataModelHandle handle, Rml::Event& event, const Rml::VariantList& args) {
|
||||
if (!g_phone.dial_number.empty()) {
|
||||
g_phone.dial_number.pop_back();
|
||||
handle.DirtyVariable("dial_number");
|
||||
}
|
||||
});
|
||||
|
||||
constructor.BindEventCallback("dial_clear",
|
||||
[](Rml::DataModelHandle handle, Rml::Event& event, const Rml::VariantList& args) {
|
||||
g_phone.dial_number.clear();
|
||||
handle.DirtyVariable("dial_number");
|
||||
});
|
||||
|
||||
constructor.BindEventCallback("make_call",
|
||||
[](Rml::DataModelHandle handle, Rml::Event& event, const Rml::VariantList& args) {
|
||||
if (!g_phone.dial_number.empty()) {
|
||||
g_phone.is_calling = true;
|
||||
g_phone.call_contact = g_phone.dial_number;
|
||||
std::cout << "Calling: " << g_phone.call_contact << std::endl;
|
||||
handle.DirtyVariable("is_calling");
|
||||
handle.DirtyVariable("call_contact");
|
||||
}
|
||||
});
|
||||
|
||||
constructor.BindEventCallback("end_call",
|
||||
[](Rml::DataModelHandle handle, Rml::Event& event, const Rml::VariantList& args) {
|
||||
g_phone.is_calling = false;
|
||||
g_phone.call_contact = "";
|
||||
g_phone.call_duration = "00:00";
|
||||
handle.DirtyVariable("is_calling");
|
||||
handle.DirtyVariable("call_contact");
|
||||
handle.DirtyVariable("call_duration");
|
||||
});
|
||||
|
||||
g_phone_model = constructor.GetModelHandle();
|
||||
std::cout << "Phone data model created" << std::endl;
|
||||
}
|
||||
|
||||
// Browser data model
|
||||
if (auto constructor = context->CreateDataModel("browser"))
|
||||
{
|
||||
if (auto tab_handle = constructor.RegisterStruct<BrowserTab>())
|
||||
{
|
||||
tab_handle.RegisterMember("url", &BrowserTab::url);
|
||||
tab_handle.RegisterMember("title", &BrowserTab::title);
|
||||
tab_handle.RegisterMember("is_loading", &BrowserTab::is_loading);
|
||||
}
|
||||
|
||||
constructor.RegisterArray<std::vector<BrowserTab>>();
|
||||
|
||||
constructor.Bind("current_url", &g_browser.current_url);
|
||||
constructor.Bind("page_title", &g_browser.page_title);
|
||||
constructor.Bind("page_content", &g_browser.page_content);
|
||||
constructor.Bind("tabs", &g_browser.tabs);
|
||||
constructor.Bind("current_tab", &g_browser.current_tab);
|
||||
constructor.Bind("can_go_back", &g_browser.can_go_back);
|
||||
constructor.Bind("can_go_forward", &g_browser.can_go_forward);
|
||||
|
||||
constructor.BindEventCallback("navigate",
|
||||
[](Rml::DataModelHandle handle, Rml::Event& event, const Rml::VariantList& args) {
|
||||
if (!args.empty()) {
|
||||
g_browser.current_url = args[0].Get<Rml::String>();
|
||||
g_browser.page_title = "Loading...";
|
||||
g_browser.can_go_back = true;
|
||||
std::cout << "Navigating to: " << g_browser.current_url << std::endl;
|
||||
handle.DirtyVariable("current_url");
|
||||
handle.DirtyVariable("page_title");
|
||||
handle.DirtyVariable("can_go_back");
|
||||
}
|
||||
});
|
||||
|
||||
constructor.BindEventCallback("refresh",
|
||||
[](Rml::DataModelHandle handle, Rml::Event& event, const Rml::VariantList& args) {
|
||||
std::cout << "Refreshing page" << std::endl;
|
||||
});
|
||||
|
||||
g_browser_model = constructor.GetModelHandle();
|
||||
std::cout << "Browser data model created" << std::endl;
|
||||
}
|
||||
|
||||
std::cout << "All data models initialized" << std::endl;
|
||||
}
|
||||
99
designer/src/data_models.h
Normal file
99
designer/src/data_models.h
Normal file
@@ -0,0 +1,99 @@
|
||||
// Data models for Mosis phone UI
|
||||
#pragma once
|
||||
|
||||
#include <RmlUi/Core.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// Settings data model
|
||||
struct SettingsData {
|
||||
bool wifi = true;
|
||||
bool bluetooth = false;
|
||||
bool airplane_mode = false;
|
||||
bool location = true;
|
||||
bool dark_mode = true;
|
||||
bool notifications = true;
|
||||
bool do_not_disturb = false;
|
||||
|
||||
// User profile
|
||||
Rml::String user_name = "User Name";
|
||||
Rml::String user_email = "user@example.com";
|
||||
|
||||
// Device info
|
||||
Rml::String wifi_network = "Home_Network";
|
||||
int battery_percent = 85;
|
||||
Rml::String battery_remaining = "About 12h remaining";
|
||||
Rml::String storage_used = "45.2 GB used of 128 GB";
|
||||
};
|
||||
|
||||
// Phone state data model
|
||||
struct PhoneData {
|
||||
Rml::String dial_number = "";
|
||||
bool is_calling = false;
|
||||
Rml::String call_contact = "";
|
||||
Rml::String call_duration = "00:00";
|
||||
};
|
||||
|
||||
// Messages data model
|
||||
struct Message {
|
||||
Rml::String from;
|
||||
Rml::String text;
|
||||
Rml::String time;
|
||||
};
|
||||
|
||||
struct Conversation {
|
||||
int id;
|
||||
Rml::String name;
|
||||
Rml::String color;
|
||||
Rml::String last_message;
|
||||
Rml::String time;
|
||||
int unread;
|
||||
std::vector<Message> messages;
|
||||
};
|
||||
|
||||
// Contact data model
|
||||
struct Contact {
|
||||
int id;
|
||||
Rml::String name;
|
||||
Rml::String phone;
|
||||
Rml::String email;
|
||||
Rml::String color;
|
||||
Rml::String initial;
|
||||
};
|
||||
|
||||
// Browser data model
|
||||
struct BrowserTab {
|
||||
Rml::String url;
|
||||
Rml::String title;
|
||||
bool is_loading;
|
||||
};
|
||||
|
||||
struct BrowserData {
|
||||
Rml::String current_url = "example.com";
|
||||
Rml::String page_title = "Example Domain";
|
||||
Rml::String page_content = "This domain is for use in illustrative examples.";
|
||||
std::vector<BrowserTab> tabs;
|
||||
int current_tab = 0;
|
||||
bool can_go_back = false;
|
||||
bool can_go_forward = false;
|
||||
};
|
||||
|
||||
// Global data instances
|
||||
extern SettingsData g_settings;
|
||||
extern PhoneData g_phone;
|
||||
extern BrowserData g_browser;
|
||||
extern std::vector<Conversation> g_conversations;
|
||||
extern std::vector<Contact> g_contacts;
|
||||
extern int g_selected_conversation;
|
||||
extern int g_selected_contact;
|
||||
|
||||
// Data model handles
|
||||
extern Rml::DataModelHandle g_settings_model;
|
||||
extern Rml::DataModelHandle g_phone_model;
|
||||
extern Rml::DataModelHandle g_messages_model;
|
||||
extern Rml::DataModelHandle g_contacts_model;
|
||||
extern Rml::DataModelHandle g_browser_model;
|
||||
|
||||
// Functions
|
||||
void initializeSampleData();
|
||||
void setupDataModels(Rml::Context* context);
|
||||
89
designer/src/desktop_platform.cpp
Normal file
89
designer/src/desktop_platform.cpp
Normal file
@@ -0,0 +1,89 @@
|
||||
// Desktop platform implementation (simplified - uses RmlUi backend for graphics)
|
||||
#include "desktop_platform.h"
|
||||
#include <iostream>
|
||||
#include <chrono>
|
||||
|
||||
// Note: Graphics context and rendering is handled by RmlUi's backend.
|
||||
// This platform implementation provides additional utilities and state management.
|
||||
|
||||
namespace mosis::desktop {
|
||||
|
||||
DesktopPlatform::DesktopPlatform()
|
||||
: m_file_interface(std::make_unique<DesktopFileInterface>())
|
||||
{
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
m_start_time = std::chrono::duration<double>(now.time_since_epoch()).count();
|
||||
}
|
||||
|
||||
DesktopPlatform::~DesktopPlatform() = default;
|
||||
|
||||
bool DesktopPlatform::Initialize(uint32_t width, uint32_t height, const char* title) {
|
||||
m_width = width;
|
||||
m_height = height;
|
||||
// Graphics initialization is done by RmlUi Backend
|
||||
return true;
|
||||
}
|
||||
|
||||
void DesktopPlatform::Shutdown() {
|
||||
// Graphics shutdown is done by RmlUi Backend
|
||||
}
|
||||
|
||||
std::unique_ptr<IGraphicsContext> DesktopPlatform::CreateGraphicsContext() {
|
||||
// Graphics context is managed by RmlUi Backend
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::unique_ptr<IRenderTarget> DesktopPlatform::CreateRenderTarget(uint32_t width, uint32_t height) {
|
||||
// Render targets are managed by RmlUi Backend
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
IFileInterface& DesktopPlatform::GetFileInterface() {
|
||||
return *m_file_interface;
|
||||
}
|
||||
|
||||
void DesktopPlatform::Log(const std::string& message) {
|
||||
std::cout << "[INFO] " << message << std::endl;
|
||||
}
|
||||
|
||||
void DesktopPlatform::LogError(const std::string& message) {
|
||||
std::cerr << "[ERROR] " << message << std::endl;
|
||||
}
|
||||
|
||||
bool DesktopPlatform::PollEvents() {
|
||||
// Events are handled by RmlUi Backend
|
||||
return true;
|
||||
}
|
||||
|
||||
void DesktopPlatform::SwapBuffers() {
|
||||
// Swap is handled by RmlUi Backend
|
||||
}
|
||||
|
||||
bool DesktopPlatform::ShouldClose() const {
|
||||
return false; // Determined by RmlUi Backend
|
||||
}
|
||||
|
||||
void DesktopPlatform::SetResolution(uint32_t width, uint32_t height) {
|
||||
m_width = width;
|
||||
m_height = height;
|
||||
}
|
||||
|
||||
float DesktopPlatform::GetDpiScale() const {
|
||||
return 1.0f;
|
||||
}
|
||||
|
||||
double DesktopPlatform::GetElapsedTime() const {
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
double current = std::chrono::duration<double>(now.time_since_epoch()).count();
|
||||
return current - m_start_time;
|
||||
}
|
||||
|
||||
bool DesktopPlatform::IsMouseButtonDown() const {
|
||||
return false; // Input is handled through RmlUi
|
||||
}
|
||||
|
||||
void DesktopPlatform::GetMousePosition(double& x, double& y) const {
|
||||
x = y = 0; // Input is handled through RmlUi
|
||||
}
|
||||
|
||||
} // namespace mosis::desktop
|
||||
50
designer/src/desktop_platform.h
Normal file
50
designer/src/desktop_platform.h
Normal file
@@ -0,0 +1,50 @@
|
||||
// Desktop platform implementation using GLFW + OpenGL 3.3
|
||||
// Note: Graphics context and rendering is handled by RmlUi's backend.
|
||||
// This platform implementation provides file interface and utilities.
|
||||
#pragma once
|
||||
|
||||
#include "platform.h"
|
||||
#include "file_interface.h"
|
||||
#include <memory>
|
||||
|
||||
namespace mosis::desktop {
|
||||
|
||||
class DesktopPlatform : public IPlatform {
|
||||
uint32_t m_width = 540;
|
||||
uint32_t m_height = 960;
|
||||
std::unique_ptr<DesktopFileInterface> m_file_interface;
|
||||
double m_start_time = 0.0;
|
||||
|
||||
public:
|
||||
DesktopPlatform();
|
||||
~DesktopPlatform() override;
|
||||
|
||||
// Initialize the platform
|
||||
bool Initialize(uint32_t width, uint32_t height, const char* title);
|
||||
void Shutdown();
|
||||
|
||||
// IPlatform implementation
|
||||
std::unique_ptr<IGraphicsContext> CreateGraphicsContext() override;
|
||||
std::unique_ptr<IRenderTarget> CreateRenderTarget(uint32_t width, uint32_t height) override;
|
||||
IFileInterface& GetFileInterface() override;
|
||||
|
||||
void Log(const std::string& message) override;
|
||||
void LogError(const std::string& message) override;
|
||||
|
||||
bool PollEvents() override;
|
||||
void SwapBuffers() override;
|
||||
bool ShouldClose() const override;
|
||||
|
||||
uint32_t GetWidth() const override { return m_width; }
|
||||
uint32_t GetHeight() const override { return m_height; }
|
||||
void SetResolution(uint32_t width, uint32_t height) override;
|
||||
float GetDpiScale() const override;
|
||||
|
||||
double GetElapsedTime() const override;
|
||||
|
||||
// Input state (delegated to RmlUi backend)
|
||||
bool IsMouseButtonDown() const;
|
||||
void GetMousePosition(double& x, double& y) const;
|
||||
};
|
||||
|
||||
} // namespace mosis::desktop
|
||||
93
designer/src/hot_reload.cpp
Normal file
93
designer/src/hot_reload.cpp
Normal file
@@ -0,0 +1,93 @@
|
||||
// Hot-reload file watcher implementation
|
||||
#include "hot_reload.h"
|
||||
#include <iostream>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <Windows.h>
|
||||
#endif
|
||||
|
||||
namespace mosis::desktop {
|
||||
|
||||
HotReload::HotReload(const std::filesystem::path& watch_path, ReloadCallback callback)
|
||||
: m_watch_path(watch_path)
|
||||
, m_callback(std::move(callback))
|
||||
, m_last_change(std::chrono::steady_clock::now())
|
||||
{}
|
||||
|
||||
HotReload::~HotReload() {
|
||||
Stop();
|
||||
}
|
||||
|
||||
void HotReload::Start() {
|
||||
if (m_running) return;
|
||||
|
||||
#ifdef _WIN32
|
||||
m_notification_handle = FindFirstChangeNotificationW(
|
||||
m_watch_path.c_str(),
|
||||
TRUE, // Watch subtree
|
||||
FILE_NOTIFY_CHANGE_LAST_WRITE
|
||||
);
|
||||
|
||||
if (m_notification_handle == INVALID_HANDLE_VALUE) {
|
||||
std::cerr << "Failed to create file change notification for: "
|
||||
<< m_watch_path << std::endl;
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
m_running = true;
|
||||
m_watch_thread = std::thread(&HotReload::WatchThread, this);
|
||||
}
|
||||
|
||||
void HotReload::Stop() {
|
||||
m_running = false;
|
||||
|
||||
if (m_watch_thread.joinable()) {
|
||||
m_watch_thread.join();
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
if (m_notification_handle && m_notification_handle != INVALID_HANDLE_VALUE) {
|
||||
FindCloseChangeNotification(m_notification_handle);
|
||||
m_notification_handle = nullptr;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
bool HotReload::CheckForChanges() {
|
||||
if (m_change_detected) {
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
if (now - m_last_change >= m_debounce_delay) {
|
||||
m_change_detected = false;
|
||||
if (m_callback) {
|
||||
m_callback();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void HotReload::WatchThread() {
|
||||
while (m_running) {
|
||||
#ifdef _WIN32
|
||||
DWORD result = WaitForSingleObject(m_notification_handle, 100); // 100ms timeout
|
||||
if (result == WAIT_OBJECT_0) {
|
||||
m_last_change = std::chrono::steady_clock::now();
|
||||
m_change_detected = true;
|
||||
|
||||
// Reset the notification
|
||||
if (!FindNextChangeNotification(m_notification_handle)) {
|
||||
std::cerr << "FindNextChangeNotification failed" << std::endl;
|
||||
break;
|
||||
}
|
||||
}
|
||||
#else
|
||||
// TODO: Linux inotify / macOS FSEvents implementation
|
||||
// For now, just sleep to avoid busy loop
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace mosis::desktop
|
||||
44
designer/src/hot_reload.h
Normal file
44
designer/src/hot_reload.h
Normal file
@@ -0,0 +1,44 @@
|
||||
// Hot-reload file watcher for development
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <thread>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
|
||||
namespace mosis::desktop {
|
||||
|
||||
class HotReload {
|
||||
public:
|
||||
using ReloadCallback = std::function<void()>;
|
||||
|
||||
HotReload(const std::filesystem::path& watch_path, ReloadCallback callback);
|
||||
~HotReload();
|
||||
|
||||
void Start();
|
||||
void Stop();
|
||||
bool IsRunning() const { return m_running; }
|
||||
|
||||
// Check for changes (call from main thread if not using threaded mode)
|
||||
bool CheckForChanges();
|
||||
|
||||
private:
|
||||
void WatchThread();
|
||||
|
||||
std::filesystem::path m_watch_path;
|
||||
ReloadCallback m_callback;
|
||||
std::thread m_watch_thread;
|
||||
std::atomic<bool> m_running{false};
|
||||
std::atomic<bool> m_change_detected{false};
|
||||
|
||||
#ifdef _WIN32
|
||||
void* m_notification_handle = nullptr; // HANDLE
|
||||
#endif
|
||||
|
||||
// Debounce settings
|
||||
std::chrono::milliseconds m_debounce_delay{100};
|
||||
std::chrono::steady_clock::time_point m_last_change;
|
||||
};
|
||||
|
||||
} // namespace mosis::desktop
|
||||
330
designer/src/kernel_impl.cpp
Normal file
330
designer/src/kernel_impl.cpp
Normal file
@@ -0,0 +1,330 @@
|
||||
// Desktop kernel implementation
|
||||
#include "kernel_impl.h"
|
||||
#include "platform.h"
|
||||
#include "file_interface.h"
|
||||
#include "log.h"
|
||||
#include <RmlUi/Core.h>
|
||||
#include <RmlUi/Lua.h>
|
||||
#include <RmlUi/Lua/Interpreter.h>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
#include <functional>
|
||||
#include <atomic>
|
||||
#include <iostream>
|
||||
#include <chrono>
|
||||
#include <algorithm>
|
||||
|
||||
extern "C" {
|
||||
#include <lua.h>
|
||||
#include <lauxlib.h>
|
||||
#include <lualib.h>
|
||||
}
|
||||
|
||||
namespace mosis {
|
||||
|
||||
// Global kernel instance for Lua access
|
||||
static DesktopKernel* g_kernel_instance = nullptr;
|
||||
|
||||
// Implementation class
|
||||
class DesktopKernel::Impl {
|
||||
public:
|
||||
KernelConfig config;
|
||||
Rml::Context* context = nullptr;
|
||||
Rml::ElementDocument* document = nullptr;
|
||||
std::vector<std::shared_ptr<IServiceListener>> listeners;
|
||||
std::vector<std::function<void()>> tasks;
|
||||
std::mutex mutex;
|
||||
std::atomic<bool> running{false};
|
||||
std::atomic<bool> reload_requested{false};
|
||||
std::string current_document_path;
|
||||
|
||||
// Status bar time
|
||||
std::string status_time;
|
||||
Rml::DataModelHandle status_time_handle;
|
||||
|
||||
explicit Impl(const KernelConfig& cfg) : config(cfg) {}
|
||||
};
|
||||
|
||||
DesktopKernel::DesktopKernel(const KernelConfig& config)
|
||||
: m_impl(std::make_unique<Impl>(config))
|
||||
{
|
||||
g_kernel_instance = this;
|
||||
}
|
||||
|
||||
DesktopKernel::~DesktopKernel() {
|
||||
Stop();
|
||||
g_kernel_instance = nullptr;
|
||||
}
|
||||
|
||||
void DesktopKernel::AddListener(std::shared_ptr<IServiceListener> listener) {
|
||||
std::lock_guard lock(m_impl->mutex);
|
||||
m_impl->listeners.push_back(listener);
|
||||
if (m_impl->running) {
|
||||
listener->OnServiceInitialized(true);
|
||||
}
|
||||
}
|
||||
|
||||
void DesktopKernel::RemoveListener(std::shared_ptr<IServiceListener> listener) {
|
||||
std::lock_guard lock(m_impl->mutex);
|
||||
m_impl->listeners.erase(
|
||||
std::remove(m_impl->listeners.begin(), m_impl->listeners.end(), listener),
|
||||
m_impl->listeners.end()
|
||||
);
|
||||
}
|
||||
|
||||
void DesktopKernel::OnTouchDown(float x, float y) {
|
||||
std::lock_guard lock(m_impl->mutex);
|
||||
auto px = static_cast<int>(x * m_impl->config.width);
|
||||
auto py = static_cast<int>(y * m_impl->config.height);
|
||||
m_impl->tasks.emplace_back([this, px, py]() {
|
||||
if (m_impl->context) {
|
||||
m_impl->context->ProcessMouseMove(px, py, 0);
|
||||
m_impl->context->ProcessMouseButtonDown(0, 0);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void DesktopKernel::OnTouchMove(float x, float y) {
|
||||
std::lock_guard lock(m_impl->mutex);
|
||||
auto px = static_cast<int>(x * m_impl->config.width);
|
||||
auto py = static_cast<int>(y * m_impl->config.height);
|
||||
m_impl->tasks.emplace_back([this, px, py]() {
|
||||
if (m_impl->context) {
|
||||
m_impl->context->ProcessMouseMove(px, py, 0);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void DesktopKernel::OnTouchUp(float x, float y) {
|
||||
std::lock_guard lock(m_impl->mutex);
|
||||
auto px = static_cast<int>(x * m_impl->config.width);
|
||||
auto py = static_cast<int>(y * m_impl->config.height);
|
||||
m_impl->tasks.emplace_back([this, px, py]() {
|
||||
if (m_impl->context) {
|
||||
m_impl->context->ProcessMouseMove(px, py, 0);
|
||||
m_impl->context->ProcessMouseButtonUp(0, 0);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void DesktopKernel::OnBackButton() {
|
||||
std::lock_guard lock(m_impl->mutex);
|
||||
m_impl->tasks.emplace_back([this]() {
|
||||
lua_State* L = Rml::Lua::Interpreter::GetLuaState();
|
||||
lua_getglobal(L, "goBack");
|
||||
if (lua_isfunction(L, -1)) {
|
||||
lua_pcall(L, 0, 0, 0);
|
||||
} else {
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void DesktopKernel::OnHomeButton() {
|
||||
std::lock_guard lock(m_impl->mutex);
|
||||
m_impl->tasks.emplace_back([this]() {
|
||||
lua_State* L = Rml::Lua::Interpreter::GetLuaState();
|
||||
lua_getglobal(L, "goHome");
|
||||
if (lua_isfunction(L, -1)) {
|
||||
lua_pcall(L, 0, 0, 0);
|
||||
} else {
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void DesktopKernel::OnRecentsButton() {
|
||||
std::cout << "Recents button pressed (not implemented)" << std::endl;
|
||||
}
|
||||
|
||||
void DesktopKernel::RequestReload() {
|
||||
m_impl->reload_requested = true;
|
||||
}
|
||||
|
||||
void DesktopKernel::LoadDocument(const std::string& path) {
|
||||
std::lock_guard lock(m_impl->mutex);
|
||||
m_impl->tasks.emplace_back([this, path]() {
|
||||
auto& file_interface = GetPlatform().GetFileInterface();
|
||||
std::string full_path = file_interface.ResolvePath(path);
|
||||
|
||||
if (m_impl->document) {
|
||||
m_impl->context->UnloadDocument(m_impl->document);
|
||||
m_impl->document = nullptr;
|
||||
}
|
||||
|
||||
m_impl->document = m_impl->context->LoadDocument(full_path);
|
||||
if (m_impl->document) {
|
||||
m_impl->document->Show();
|
||||
m_impl->current_document_path = full_path;
|
||||
std::cout << "Loaded document: " << path << std::endl;
|
||||
} else {
|
||||
std::cerr << "Failed to load document: " << path << std::endl;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void DesktopKernel::Start() {
|
||||
if (m_impl->running) return;
|
||||
m_impl->running = true;
|
||||
|
||||
std::lock_guard lock(m_impl->mutex);
|
||||
for (auto& listener : m_impl->listeners) {
|
||||
listener->OnServiceInitialized(true);
|
||||
}
|
||||
}
|
||||
|
||||
void DesktopKernel::Stop() {
|
||||
m_impl->running = false;
|
||||
}
|
||||
|
||||
bool DesktopKernel::IsRunning() const {
|
||||
return m_impl->running;
|
||||
}
|
||||
|
||||
void DesktopKernel::Update() {
|
||||
if (!m_impl->running || !m_impl->context) return;
|
||||
|
||||
// Process pending tasks
|
||||
{
|
||||
std::lock_guard lock(m_impl->mutex);
|
||||
for (const auto& task : m_impl->tasks) {
|
||||
task();
|
||||
}
|
||||
m_impl->tasks.clear();
|
||||
}
|
||||
|
||||
// Handle reload request
|
||||
if (m_impl->reload_requested) {
|
||||
m_impl->reload_requested = false;
|
||||
|
||||
if (!m_impl->current_document_path.empty()) {
|
||||
std::cout << "Reloading document..." << std::endl;
|
||||
|
||||
if (m_impl->document) {
|
||||
m_impl->context->UnloadDocument(m_impl->document);
|
||||
m_impl->document = nullptr;
|
||||
}
|
||||
|
||||
m_impl->context->Update();
|
||||
|
||||
m_impl->document = m_impl->context->LoadDocument(m_impl->current_document_path);
|
||||
if (m_impl->document) {
|
||||
m_impl->document->ReloadStyleSheet();
|
||||
m_impl->document->Show();
|
||||
std::cout << "Document reloaded" << std::endl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update status bar time
|
||||
auto now = std::chrono::system_clock::now();
|
||||
auto time_t = std::chrono::system_clock::to_time_t(now);
|
||||
std::tm tm{};
|
||||
#ifdef _WIN32
|
||||
localtime_s(&tm, &time_t);
|
||||
#else
|
||||
localtime_r(&time_t, &tm);
|
||||
#endif
|
||||
char buffer[16];
|
||||
std::strftime(buffer, sizeof(buffer), "%H:%M", &tm);
|
||||
m_impl->status_time = buffer;
|
||||
|
||||
if (m_impl->status_time_handle) {
|
||||
m_impl->status_time_handle.DirtyVariable("time");
|
||||
}
|
||||
|
||||
m_impl->context->Update();
|
||||
}
|
||||
|
||||
void DesktopKernel::Render() {
|
||||
if (!m_impl->running || !m_impl->context) return;
|
||||
m_impl->context->Render();
|
||||
|
||||
std::lock_guard lock(m_impl->mutex);
|
||||
for (auto& listener : m_impl->listeners) {
|
||||
listener->OnFrameAvailable();
|
||||
}
|
||||
}
|
||||
|
||||
void DesktopKernel::SetContext(Rml::Context* context) {
|
||||
m_impl->context = context;
|
||||
|
||||
// Setup status data model
|
||||
if (m_impl->context) {
|
||||
if (auto constructor = m_impl->context->CreateDataModel("status-data")) {
|
||||
constructor.Bind("time", &m_impl->status_time);
|
||||
m_impl->status_time_handle = constructor.GetModelHandle();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rml::Context* DesktopKernel::GetContext() const {
|
||||
return m_impl->context;
|
||||
}
|
||||
|
||||
void DesktopKernel::SetDocument(Rml::ElementDocument* doc) {
|
||||
m_impl->document = doc;
|
||||
}
|
||||
|
||||
Rml::ElementDocument* DesktopKernel::GetDocument() const {
|
||||
return m_impl->document;
|
||||
}
|
||||
|
||||
void DesktopKernel::SetCurrentDocumentPath(const std::string& path) {
|
||||
m_impl->current_document_path = path;
|
||||
}
|
||||
|
||||
int DesktopKernel::LuaLoadScreen(lua_State* L) {
|
||||
if (!g_kernel_instance) {
|
||||
lua_pushboolean(L, false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const char* path = luaL_checkstring(L, 1);
|
||||
Log(std::string("Loading screen: ") + path);
|
||||
|
||||
auto& file_interface = GetPlatform().GetFileInterface();
|
||||
std::string full_path = file_interface.ResolvePath(path);
|
||||
|
||||
if (!file_interface.FileExists(full_path)) {
|
||||
Log(std::string("Screen not found: ") + full_path);
|
||||
lua_pushboolean(L, false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
auto* impl = g_kernel_instance->m_impl.get();
|
||||
|
||||
// Unload current document
|
||||
if (impl->document) {
|
||||
impl->context->UnloadDocument(impl->document);
|
||||
impl->document = nullptr;
|
||||
}
|
||||
|
||||
// Load new document
|
||||
impl->document = impl->context->LoadDocument(full_path);
|
||||
if (impl->document) {
|
||||
impl->document->Show();
|
||||
impl->current_document_path = full_path;
|
||||
Log(std::string("Loaded screen: ") + path);
|
||||
lua_pushboolean(L, true);
|
||||
} else {
|
||||
Log(std::string("Failed to load screen: ") + path);
|
||||
lua_pushboolean(L, false);
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
void DesktopKernel::RegisterLuaFunctions() {
|
||||
lua_State* L = Rml::Lua::Interpreter::GetLuaState();
|
||||
lua_pushcfunction(L, LuaLoadScreen);
|
||||
lua_setglobal(L, "loadScreen");
|
||||
std::cout << "Registered Lua loadScreen function" << std::endl;
|
||||
}
|
||||
|
||||
// Factory function
|
||||
std::unique_ptr<IKernel> CreateKernel(const KernelConfig& config) {
|
||||
return std::make_unique<DesktopKernel>(config);
|
||||
}
|
||||
|
||||
} // namespace mosis
|
||||
57
designer/src/kernel_impl.h
Normal file
57
designer/src/kernel_impl.h
Normal file
@@ -0,0 +1,57 @@
|
||||
// Desktop kernel header
|
||||
#pragma once
|
||||
|
||||
#include "service_interface.h"
|
||||
#include <RmlUi/Core/Types.h>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
// Forward declarations
|
||||
namespace Rml {
|
||||
class Context;
|
||||
class ElementDocument;
|
||||
}
|
||||
struct lua_State;
|
||||
|
||||
namespace mosis {
|
||||
|
||||
// Desktop kernel implementation
|
||||
class DesktopKernel : public IKernel {
|
||||
public:
|
||||
explicit DesktopKernel(const KernelConfig& config);
|
||||
~DesktopKernel() override;
|
||||
|
||||
// IKernel implementation
|
||||
void AddListener(std::shared_ptr<IServiceListener> listener) override;
|
||||
void RemoveListener(std::shared_ptr<IServiceListener> listener) override;
|
||||
void OnTouchDown(float x, float y) override;
|
||||
void OnTouchMove(float x, float y) override;
|
||||
void OnTouchUp(float x, float y) override;
|
||||
void OnBackButton() override;
|
||||
void OnHomeButton() override;
|
||||
void OnRecentsButton() override;
|
||||
void RequestReload() override;
|
||||
void LoadDocument(const std::string& path) override;
|
||||
void Start() override;
|
||||
void Stop() override;
|
||||
bool IsRunning() const override;
|
||||
void Update() override;
|
||||
void Render() override;
|
||||
|
||||
// Desktop-specific methods
|
||||
void SetContext(Rml::Context* context);
|
||||
Rml::Context* GetContext() const;
|
||||
void SetDocument(Rml::ElementDocument* doc);
|
||||
Rml::ElementDocument* GetDocument() const;
|
||||
void SetCurrentDocumentPath(const std::string& path);
|
||||
|
||||
// Static Lua function registration
|
||||
static int LuaLoadScreen(lua_State* L);
|
||||
static void RegisterLuaFunctions();
|
||||
|
||||
private:
|
||||
class Impl;
|
||||
std::unique_ptr<Impl> m_impl;
|
||||
};
|
||||
|
||||
} // namespace mosis
|
||||
22
designer/src/log.h
Normal file
22
designer/src/log.h
Normal file
@@ -0,0 +1,22 @@
|
||||
// Logging utility for Mosis Designer
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
|
||||
namespace mosis {
|
||||
|
||||
// Global log file - defined in main.cpp
|
||||
extern std::ofstream* g_log_file_ptr;
|
||||
|
||||
// Log function that writes to both stdout and file
|
||||
inline void Log(const std::string& message) {
|
||||
std::cout << message << std::endl;
|
||||
if (g_log_file_ptr && g_log_file_ptr->is_open()) {
|
||||
*g_log_file_ptr << message << std::endl;
|
||||
g_log_file_ptr->flush();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace mosis
|
||||
379
designer/src/main.cpp
Normal file
379
designer/src/main.cpp
Normal file
@@ -0,0 +1,379 @@
|
||||
// Mosis Designer - Desktop designer and testing tool for Mosis virtual phone UI
|
||||
#include <iostream>
|
||||
#include <fstream>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <chrono>
|
||||
|
||||
#include <RmlUi/Core.h>
|
||||
#include <RmlUi/Debugger.h>
|
||||
#include <RmlUi/Lua.h>
|
||||
#include <RmlUi/Lua/Interpreter.h>
|
||||
#include <RmlUi_Backend.h>
|
||||
|
||||
#include "platform.h"
|
||||
#include "file_interface.h"
|
||||
#include "service_interface.h"
|
||||
#include "kernel_impl.h"
|
||||
#include "data_models.h"
|
||||
#include "hot_reload.h"
|
||||
#include "desktop_platform.h"
|
||||
#include "testing/ui_inspector.h"
|
||||
|
||||
// Command-line options
|
||||
struct Options {
|
||||
std::string document_path;
|
||||
uint32_t width = 540;
|
||||
uint32_t height = 960;
|
||||
bool dump_mode = false;
|
||||
bool debug_mode = false;
|
||||
std::string output_dir = "dump";
|
||||
std::string log_file; // If set, write logs to this file
|
||||
std::string hierarchy_file; // If set, dump UI hierarchy to this file each frame
|
||||
};
|
||||
|
||||
// Global log file stream
|
||||
static std::ofstream g_log_file;
|
||||
|
||||
// Pointer for shared logging (used by kernel_impl.cpp via log.h)
|
||||
namespace mosis {
|
||||
std::ofstream* g_log_file_ptr = nullptr;
|
||||
}
|
||||
|
||||
// Log function that writes to both stdout and file
|
||||
void LogMessage(const std::string& message) {
|
||||
std::cout << message << std::endl;
|
||||
if (g_log_file.is_open()) {
|
||||
g_log_file << message << std::endl;
|
||||
g_log_file.flush();
|
||||
}
|
||||
}
|
||||
|
||||
// Forward declarations
|
||||
void PrintUsage(const char* program);
|
||||
Options ParseOptions(int argc, const char* argv[]);
|
||||
void LoadFonts(const std::filesystem::path& fonts_path);
|
||||
std::filesystem::path FindAssetsPath(const std::filesystem::path& start_path);
|
||||
|
||||
// Custom system interface with logging
|
||||
class DesignerSystemInterface : public Rml::SystemInterface {
|
||||
Rml::SystemInterface* m_backend_interface;
|
||||
|
||||
public:
|
||||
explicit DesignerSystemInterface(Rml::SystemInterface* backend)
|
||||
: m_backend_interface(backend) {}
|
||||
|
||||
double GetElapsedTime() override {
|
||||
return m_backend_interface ? m_backend_interface->GetElapsedTime() : 0.0;
|
||||
}
|
||||
|
||||
bool LogMessage(Rml::Log::Type type, const Rml::String& message) override {
|
||||
const char* type_str = "INFO";
|
||||
switch (type) {
|
||||
case Rml::Log::LT_ERROR: type_str = "ERROR"; break;
|
||||
case Rml::Log::LT_WARNING: type_str = "WARNING"; break;
|
||||
case Rml::Log::LT_INFO: type_str = "INFO"; break;
|
||||
case Rml::Log::LT_DEBUG: type_str = "DEBUG"; break;
|
||||
default: break;
|
||||
}
|
||||
std::cout << "[RmlUi " << type_str << "] " << message << std::endl;
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
// Global state
|
||||
static DesignerSystemInterface* g_system_interface = nullptr;
|
||||
static mosis::DesktopFileInterface* g_file_interface = nullptr;
|
||||
static mosis::IKernel* g_kernel = nullptr;
|
||||
static std::filesystem::path g_assets_path;
|
||||
static mosis::testing::UIInspector g_ui_inspector;
|
||||
|
||||
|
||||
int main(int argc, const char* argv[])
|
||||
{
|
||||
// Parse command-line options first
|
||||
Options opts = ParseOptions(argc, argv);
|
||||
|
||||
// Open log file if specified
|
||||
if (!opts.log_file.empty()) {
|
||||
g_log_file.open(opts.log_file, std::ios::out | std::ios::trunc);
|
||||
if (!g_log_file.is_open()) {
|
||||
std::cerr << "Warning: Could not open log file: " << opts.log_file << std::endl;
|
||||
} else {
|
||||
mosis::g_log_file_ptr = &g_log_file;
|
||||
}
|
||||
}
|
||||
|
||||
LogMessage("Mosis Designer v0.1.0");
|
||||
|
||||
if (opts.document_path.empty()) {
|
||||
PrintUsage(argv[0]);
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
std::filesystem::path document_file = opts.document_path;
|
||||
if (!std::filesystem::exists(document_file)) {
|
||||
std::cerr << "File not found: " << opts.document_path << std::endl;
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
document_file = std::filesystem::absolute(document_file);
|
||||
|
||||
// Initialize the RmlUi backend (GLFW + OpenGL)
|
||||
if (!Backend::Initialize("Mosis Designer", opts.width, opts.height, true)) {
|
||||
std::cerr << "Failed to initialize backend" << std::endl;
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
// Find assets path
|
||||
g_assets_path = FindAssetsPath(document_file.parent_path());
|
||||
LogMessage("Assets path: " + g_assets_path.generic_string());
|
||||
|
||||
// Setup custom interfaces
|
||||
g_system_interface = new DesignerSystemInterface(Backend::GetSystemInterface());
|
||||
g_file_interface = new mosis::DesktopFileInterface(g_assets_path.string());
|
||||
|
||||
// Set platform for kernel
|
||||
auto platform = std::make_unique<mosis::desktop::DesktopPlatform>();
|
||||
platform->GetFileInterface().SetAssetsPath(g_assets_path.string());
|
||||
mosis::SetPlatform(std::move(platform));
|
||||
|
||||
Rml::SetSystemInterface(g_system_interface);
|
||||
Rml::SetFileInterface(g_file_interface);
|
||||
Rml::SetRenderInterface(Backend::GetRenderInterface());
|
||||
|
||||
// Initialize RmlUi
|
||||
Rml::Initialise();
|
||||
Rml::Lua::Initialise();
|
||||
LogMessage("RmlUi and Lua initialized");
|
||||
|
||||
// Create context
|
||||
Rml::Context* context = Rml::CreateContext("main", Rml::Vector2i(opts.width, opts.height));
|
||||
if (!context) {
|
||||
std::cerr << "Failed to create RmlUi context" << std::endl;
|
||||
Rml::Shutdown();
|
||||
Backend::Shutdown();
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
// Enable debugger in debug mode
|
||||
if (opts.debug_mode) {
|
||||
Rml::Debugger::Initialise(context);
|
||||
Rml::Debugger::SetVisible(true);
|
||||
}
|
||||
|
||||
// Load fonts
|
||||
std::filesystem::path fonts_path = g_assets_path / "fonts";
|
||||
LoadFonts(fonts_path);
|
||||
|
||||
// Initialize sample data and data models
|
||||
initializeSampleData();
|
||||
setupDataModels(context);
|
||||
|
||||
// Create and configure kernel
|
||||
mosis::KernelConfig kernel_config;
|
||||
kernel_config.width = opts.width;
|
||||
kernel_config.height = opts.height;
|
||||
kernel_config.initial_document = document_file.string();
|
||||
kernel_config.threaded = false;
|
||||
|
||||
auto kernel = mosis::CreateKernel(kernel_config);
|
||||
g_kernel = kernel.get();
|
||||
|
||||
// Set context and register Lua functions
|
||||
auto* desktop_kernel = dynamic_cast<mosis::DesktopKernel*>(g_kernel);
|
||||
if (desktop_kernel) {
|
||||
desktop_kernel->SetContext(context);
|
||||
mosis::DesktopKernel::RegisterLuaFunctions();
|
||||
}
|
||||
|
||||
// Load the initial document
|
||||
std::string document_path_str = document_file.generic_string();
|
||||
LogMessage("Loading document: " + document_path_str);
|
||||
|
||||
Rml::ElementDocument* document = context->LoadDocument(document_path_str);
|
||||
if (document) {
|
||||
document->Show();
|
||||
if (desktop_kernel) {
|
||||
desktop_kernel->SetDocument(document);
|
||||
desktop_kernel->SetCurrentDocumentPath(document_path_str);
|
||||
}
|
||||
LogMessage("Document loaded successfully");
|
||||
} else {
|
||||
LogMessage("ERROR: Failed to load document!");
|
||||
}
|
||||
|
||||
// Start kernel
|
||||
kernel->Start();
|
||||
|
||||
// Setup hot-reload
|
||||
std::unique_ptr<mosis::desktop::HotReload> hot_reload;
|
||||
if (!opts.dump_mode) {
|
||||
hot_reload = std::make_unique<mosis::desktop::HotReload>(
|
||||
g_assets_path,
|
||||
[&kernel]() {
|
||||
std::cout << "File change detected, requesting reload..." << std::endl;
|
||||
kernel->RequestReload();
|
||||
}
|
||||
);
|
||||
hot_reload->Start();
|
||||
std::cout << "Hot-reload enabled for: " << g_assets_path.generic_string() << std::endl;
|
||||
}
|
||||
|
||||
// Main loop
|
||||
bool running = true;
|
||||
while (running) {
|
||||
// Process hot-reload
|
||||
if (hot_reload) {
|
||||
hot_reload->CheckForChanges();
|
||||
}
|
||||
|
||||
// Process events and update
|
||||
running = Backend::ProcessEvents(context);
|
||||
|
||||
// Update kernel (processes tasks, updates time, etc.)
|
||||
kernel->Update();
|
||||
|
||||
// Render
|
||||
Backend::BeginFrame();
|
||||
kernel->Render();
|
||||
Backend::PresentFrame();
|
||||
|
||||
// Dump hierarchy if enabled (quiet mode to avoid spamming console)
|
||||
if (!opts.hierarchy_file.empty() && desktop_kernel) {
|
||||
Rml::ElementDocument* doc = desktop_kernel->GetDocument();
|
||||
if (doc) {
|
||||
g_ui_inspector.SaveToFile(doc, opts.hierarchy_file, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
kernel->Stop();
|
||||
kernel.reset();
|
||||
g_kernel = nullptr;
|
||||
|
||||
if (hot_reload) {
|
||||
hot_reload->Stop();
|
||||
}
|
||||
|
||||
if (opts.debug_mode) {
|
||||
Rml::Debugger::Shutdown();
|
||||
}
|
||||
|
||||
delete g_system_interface;
|
||||
delete g_file_interface;
|
||||
|
||||
Rml::Shutdown();
|
||||
Backend::Shutdown();
|
||||
|
||||
std::cout << "Mosis Designer shutdown complete" << std::endl;
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
void PrintUsage(const char* program)
|
||||
{
|
||||
std::cout << "Usage: " << program << " <document.rml> [options]" << std::endl;
|
||||
std::cout << std::endl;
|
||||
std::cout << "Options:" << std::endl;
|
||||
std::cout << " --resolution WxH Set phone resolution (default: 540x960)" << std::endl;
|
||||
std::cout << " --dump Dump mode: screenshot + hierarchy, then exit" << std::endl;
|
||||
std::cout << " --debug Enable RmlUi debugger" << std::endl;
|
||||
std::cout << " --output DIR Output directory for dump mode (default: dump)" << std::endl;
|
||||
std::cout << " --log FILE Write log output to file (for automated testing)" << std::endl;
|
||||
std::cout << " --hierarchy FILE Continuously dump UI hierarchy to JSON file" << std::endl;
|
||||
std::cout << std::endl;
|
||||
std::cout << "Examples:" << std::endl;
|
||||
std::cout << " " << program << " assets/apps/home/home.rml" << std::endl;
|
||||
std::cout << " " << program << " assets/apps/home/home.rml --resolution 720x1280" << std::endl;
|
||||
std::cout << " " << program << " assets/apps/home/home.rml --dump" << std::endl;
|
||||
}
|
||||
|
||||
Options ParseOptions(int argc, const char* argv[])
|
||||
{
|
||||
Options opts;
|
||||
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
std::string arg = argv[i];
|
||||
|
||||
if (arg == "--resolution" && i + 1 < argc) {
|
||||
std::string res = argv[++i];
|
||||
size_t x_pos = res.find('x');
|
||||
if (x_pos != std::string::npos) {
|
||||
opts.width = std::stoi(res.substr(0, x_pos));
|
||||
opts.height = std::stoi(res.substr(x_pos + 1));
|
||||
}
|
||||
} else if (arg == "--dump") {
|
||||
opts.dump_mode = true;
|
||||
} else if (arg == "--debug") {
|
||||
opts.debug_mode = true;
|
||||
} else if (arg == "--output" && i + 1 < argc) {
|
||||
opts.output_dir = argv[++i];
|
||||
} else if (arg == "--log" && i + 1 < argc) {
|
||||
opts.log_file = argv[++i];
|
||||
} else if (arg == "--hierarchy" && i + 1 < argc) {
|
||||
opts.hierarchy_file = argv[++i];
|
||||
} else if (arg[0] != '-') {
|
||||
opts.document_path = arg;
|
||||
}
|
||||
}
|
||||
|
||||
return opts;
|
||||
}
|
||||
|
||||
void LoadFonts(const std::filesystem::path& fonts_path)
|
||||
{
|
||||
if (!std::filesystem::exists(fonts_path)) {
|
||||
std::cerr << "Fonts directory not found: " << fonts_path << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
int count = 0;
|
||||
for (const auto& entry : std::filesystem::directory_iterator(fonts_path)) {
|
||||
if (entry.path().extension() == ".ttf") {
|
||||
std::string font_path = entry.path().generic_string();
|
||||
if (Rml::LoadFontFace(font_path)) {
|
||||
std::cout << "Loaded font: " << entry.path().filename() << std::endl;
|
||||
++count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also try Roboto subdirectory
|
||||
std::filesystem::path roboto_path = fonts_path / "Roboto";
|
||||
if (std::filesystem::exists(roboto_path)) {
|
||||
for (const auto& entry : std::filesystem::recursive_directory_iterator(roboto_path)) {
|
||||
if (entry.path().extension() == ".ttf") {
|
||||
std::string font_path = entry.path().generic_string();
|
||||
if (Rml::LoadFontFace(font_path)) {
|
||||
std::cout << "Loaded font: " << entry.path().filename() << std::endl;
|
||||
++count;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::cout << "Loaded " << count << " fonts" << std::endl;
|
||||
}
|
||||
|
||||
std::filesystem::path FindAssetsPath(const std::filesystem::path& start_path)
|
||||
{
|
||||
std::filesystem::path current = std::filesystem::absolute(start_path);
|
||||
|
||||
// Walk up the directory tree looking for a fonts/ subdirectory
|
||||
while (!current.empty() && current.has_parent_path()) {
|
||||
std::filesystem::path fonts_path = current / "fonts";
|
||||
if (std::filesystem::exists(fonts_path) && std::filesystem::is_directory(fonts_path)) {
|
||||
// Check if it contains TTF files
|
||||
for (const auto& entry : std::filesystem::directory_iterator(fonts_path)) {
|
||||
if (entry.path().extension() == ".ttf") {
|
||||
return current;
|
||||
}
|
||||
}
|
||||
}
|
||||
current = current.parent_path();
|
||||
}
|
||||
|
||||
// Fall back to start path
|
||||
return std::filesystem::absolute(start_path);
|
||||
}
|
||||
131
designer/src/testing/action_player.cpp
Normal file
131
designer/src/testing/action_player.cpp
Normal file
@@ -0,0 +1,131 @@
|
||||
// Action player implementation
|
||||
#include "action_player.h"
|
||||
#include "service_interface.h"
|
||||
#include <iostream>
|
||||
#include <thread>
|
||||
|
||||
namespace mosis::testing {
|
||||
|
||||
void ActionPlayer::LoadActions(const std::vector<Action>& actions) {
|
||||
m_actions = actions;
|
||||
Reset();
|
||||
}
|
||||
|
||||
void ActionPlayer::LoadFromFile(const std::string& path) {
|
||||
ActionRecorder recorder;
|
||||
recorder.LoadFromFile(path);
|
||||
m_actions = recorder.GetActions();
|
||||
Reset();
|
||||
}
|
||||
|
||||
void ActionPlayer::Play() {
|
||||
if (m_actions.empty()) {
|
||||
std::cout << "No actions to play" << std::endl;
|
||||
return;
|
||||
}
|
||||
m_playing = true;
|
||||
std::cout << "Playback started" << std::endl;
|
||||
}
|
||||
|
||||
void ActionPlayer::Pause() {
|
||||
m_playing = false;
|
||||
std::cout << "Playback paused at action " << m_current_index << std::endl;
|
||||
}
|
||||
|
||||
void ActionPlayer::Stop() {
|
||||
m_playing = false;
|
||||
Reset();
|
||||
std::cout << "Playback stopped" << std::endl;
|
||||
}
|
||||
|
||||
void ActionPlayer::Reset() {
|
||||
m_current_index = 0;
|
||||
m_elapsed_time_ms = 0;
|
||||
}
|
||||
|
||||
void ActionPlayer::StepForward() {
|
||||
if (m_current_index < m_actions.size()) {
|
||||
ExecuteAction(m_actions[m_current_index]);
|
||||
++m_current_index;
|
||||
}
|
||||
}
|
||||
|
||||
void ActionPlayer::Update(double delta_time_ms) {
|
||||
if (!m_playing || m_current_index >= m_actions.size()) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_elapsed_time_ms += delta_time_ms;
|
||||
|
||||
// Execute all actions whose timestamp has passed
|
||||
while (m_current_index < m_actions.size()) {
|
||||
const auto& action = m_actions[m_current_index];
|
||||
if (action.timestamp_ms <= m_elapsed_time_ms) {
|
||||
ExecuteAction(action);
|
||||
++m_current_index;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if finished
|
||||
if (m_current_index >= m_actions.size()) {
|
||||
m_playing = false;
|
||||
std::cout << "Playback finished" << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
void ActionPlayer::ExecuteAction(const Action& action) {
|
||||
if (!m_kernel) {
|
||||
std::cerr << "No kernel set for action player" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
switch (action.type) {
|
||||
case ActionType::Tap:
|
||||
std::cout << "Execute tap at (" << action.x << ", " << action.y << ")" << std::endl;
|
||||
m_kernel->OnTouchDown(static_cast<float>(action.x), static_cast<float>(action.y));
|
||||
m_kernel->OnTouchUp(static_cast<float>(action.x), static_cast<float>(action.y));
|
||||
break;
|
||||
|
||||
case ActionType::Swipe:
|
||||
std::cout << "Execute swipe from (" << action.x1 << ", " << action.y1
|
||||
<< ") to (" << action.x2 << ", " << action.y2 << ")" << std::endl;
|
||||
// Simplified swipe - just start and end
|
||||
m_kernel->OnTouchDown(static_cast<float>(action.x1), static_cast<float>(action.y1));
|
||||
m_kernel->OnTouchMove(static_cast<float>(action.x2), static_cast<float>(action.y2));
|
||||
m_kernel->OnTouchUp(static_cast<float>(action.x2), static_cast<float>(action.y2));
|
||||
break;
|
||||
|
||||
case ActionType::LongPress:
|
||||
std::cout << "Execute long press at (" << action.x << ", " << action.y
|
||||
<< ") for " << action.duration_ms << "ms" << std::endl;
|
||||
m_kernel->OnTouchDown(static_cast<float>(action.x), static_cast<float>(action.y));
|
||||
// In a real implementation, we'd hold for duration
|
||||
m_kernel->OnTouchUp(static_cast<float>(action.x), static_cast<float>(action.y));
|
||||
break;
|
||||
|
||||
case ActionType::Button:
|
||||
std::cout << "Execute button: " << action.button << std::endl;
|
||||
if (action.button == "back") {
|
||||
m_kernel->OnBackButton();
|
||||
} else if (action.button == "home") {
|
||||
m_kernel->OnHomeButton();
|
||||
} else if (action.button == "recents") {
|
||||
m_kernel->OnRecentsButton();
|
||||
}
|
||||
break;
|
||||
|
||||
case ActionType::Wait:
|
||||
std::cout << "Wait " << action.duration_ms << "ms" << std::endl;
|
||||
// Wait is handled by timestamp comparison
|
||||
break;
|
||||
}
|
||||
|
||||
// Call callback if set
|
||||
if (m_action_callback) {
|
||||
m_action_callback(action, m_current_index);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace mosis::testing
|
||||
60
designer/src/testing/action_player.h
Normal file
60
designer/src/testing/action_player.h
Normal file
@@ -0,0 +1,60 @@
|
||||
// Action player for replaying recorded UI interactions
|
||||
#pragma once
|
||||
|
||||
#include "action_recorder.h"
|
||||
#include <functional>
|
||||
|
||||
namespace mosis {
|
||||
class IKernel;
|
||||
}
|
||||
|
||||
namespace mosis::testing {
|
||||
|
||||
// Callback for when an action is executed
|
||||
using ActionCallback = std::function<void(const Action&, size_t index)>;
|
||||
|
||||
// Plays back recorded actions
|
||||
class ActionPlayer {
|
||||
public:
|
||||
ActionPlayer() = default;
|
||||
|
||||
// Set the kernel for executing actions
|
||||
void SetKernel(IKernel* kernel) { m_kernel = kernel; }
|
||||
|
||||
// Load actions to play
|
||||
void LoadActions(const std::vector<Action>& actions);
|
||||
void LoadFromFile(const std::string& path);
|
||||
|
||||
// Playback control
|
||||
void Play();
|
||||
void Pause();
|
||||
void Stop();
|
||||
void Reset();
|
||||
|
||||
// Step through one action at a time
|
||||
void StepForward();
|
||||
|
||||
// Update (call each frame)
|
||||
void Update(double delta_time_ms);
|
||||
|
||||
// State
|
||||
bool IsPlaying() const { return m_playing; }
|
||||
bool IsFinished() const { return m_current_index >= m_actions.size(); }
|
||||
size_t GetCurrentIndex() const { return m_current_index; }
|
||||
size_t GetActionCount() const { return m_actions.size(); }
|
||||
|
||||
// Callbacks
|
||||
void SetActionCallback(ActionCallback callback) { m_action_callback = callback; }
|
||||
|
||||
private:
|
||||
void ExecuteAction(const Action& action);
|
||||
|
||||
IKernel* m_kernel = nullptr;
|
||||
std::vector<Action> m_actions;
|
||||
size_t m_current_index = 0;
|
||||
double m_elapsed_time_ms = 0;
|
||||
bool m_playing = false;
|
||||
ActionCallback m_action_callback;
|
||||
};
|
||||
|
||||
} // namespace mosis::testing
|
||||
191
designer/src/testing/action_recorder.cpp
Normal file
191
designer/src/testing/action_recorder.cpp
Normal file
@@ -0,0 +1,191 @@
|
||||
// Action recorder implementation
|
||||
#include "action_recorder.h"
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
|
||||
namespace mosis::testing {
|
||||
|
||||
nlohmann::json Action::ToJson() const {
|
||||
nlohmann::json j;
|
||||
|
||||
switch (type) {
|
||||
case ActionType::Tap:
|
||||
j["type"] = "tap";
|
||||
j["x"] = x;
|
||||
j["y"] = y;
|
||||
break;
|
||||
case ActionType::Swipe:
|
||||
j["type"] = "swipe";
|
||||
j["x1"] = x1;
|
||||
j["y1"] = y1;
|
||||
j["x2"] = x2;
|
||||
j["y2"] = y2;
|
||||
j["duration"] = duration_ms;
|
||||
break;
|
||||
case ActionType::LongPress:
|
||||
j["type"] = "long_press";
|
||||
j["x"] = x;
|
||||
j["y"] = y;
|
||||
j["duration"] = duration_ms;
|
||||
break;
|
||||
case ActionType::Button:
|
||||
j["type"] = "button";
|
||||
j["button"] = button;
|
||||
break;
|
||||
case ActionType::Wait:
|
||||
j["type"] = "wait";
|
||||
j["duration"] = duration_ms;
|
||||
break;
|
||||
}
|
||||
|
||||
j["timestamp"] = timestamp_ms;
|
||||
return j;
|
||||
}
|
||||
|
||||
Action Action::FromJson(const nlohmann::json& j) {
|
||||
Action action;
|
||||
action.timestamp_ms = j.value("timestamp", 0);
|
||||
|
||||
std::string type_str = j.value("type", "");
|
||||
if (type_str == "tap") {
|
||||
action.type = ActionType::Tap;
|
||||
action.x = j.value("x", 0.0);
|
||||
action.y = j.value("y", 0.0);
|
||||
} else if (type_str == "swipe") {
|
||||
action.type = ActionType::Swipe;
|
||||
action.x1 = j.value("x1", 0.0);
|
||||
action.y1 = j.value("y1", 0.0);
|
||||
action.x2 = j.value("x2", 0.0);
|
||||
action.y2 = j.value("y2", 0.0);
|
||||
action.duration_ms = j.value("duration", 0);
|
||||
} else if (type_str == "long_press") {
|
||||
action.type = ActionType::LongPress;
|
||||
action.x = j.value("x", 0.0);
|
||||
action.y = j.value("y", 0.0);
|
||||
action.duration_ms = j.value("duration", 0);
|
||||
} else if (type_str == "button") {
|
||||
action.type = ActionType::Button;
|
||||
action.button = j.value("button", "");
|
||||
} else if (type_str == "wait") {
|
||||
action.type = ActionType::Wait;
|
||||
action.duration_ms = j.value("duration", 0);
|
||||
}
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
void ActionRecorder::StartRecording() {
|
||||
m_recording = true;
|
||||
m_start_time = std::chrono::steady_clock::now();
|
||||
m_actions.clear();
|
||||
std::cout << "Action recording started" << std::endl;
|
||||
}
|
||||
|
||||
void ActionRecorder::StopRecording() {
|
||||
m_recording = false;
|
||||
std::cout << "Action recording stopped. Recorded " << m_actions.size() << " actions" << std::endl;
|
||||
}
|
||||
|
||||
void ActionRecorder::RecordTap(double x, double y) {
|
||||
if (!m_recording) return;
|
||||
|
||||
Action action;
|
||||
action.type = ActionType::Tap;
|
||||
action.x = x;
|
||||
action.y = y;
|
||||
action.timestamp_ms = GetTimestamp();
|
||||
m_actions.push_back(action);
|
||||
}
|
||||
|
||||
void ActionRecorder::RecordSwipe(double x1, double y1, double x2, double y2, int duration_ms) {
|
||||
if (!m_recording) return;
|
||||
|
||||
Action action;
|
||||
action.type = ActionType::Swipe;
|
||||
action.x1 = x1;
|
||||
action.y1 = y1;
|
||||
action.x2 = x2;
|
||||
action.y2 = y2;
|
||||
action.duration_ms = duration_ms;
|
||||
action.timestamp_ms = GetTimestamp();
|
||||
m_actions.push_back(action);
|
||||
}
|
||||
|
||||
void ActionRecorder::RecordLongPress(double x, double y, int duration_ms) {
|
||||
if (!m_recording) return;
|
||||
|
||||
Action action;
|
||||
action.type = ActionType::LongPress;
|
||||
action.x = x;
|
||||
action.y = y;
|
||||
action.duration_ms = duration_ms;
|
||||
action.timestamp_ms = GetTimestamp();
|
||||
m_actions.push_back(action);
|
||||
}
|
||||
|
||||
void ActionRecorder::RecordButton(const std::string& button) {
|
||||
if (!m_recording) return;
|
||||
|
||||
Action action;
|
||||
action.type = ActionType::Button;
|
||||
action.button = button;
|
||||
action.timestamp_ms = GetTimestamp();
|
||||
m_actions.push_back(action);
|
||||
}
|
||||
|
||||
void ActionRecorder::RecordWait(int duration_ms) {
|
||||
if (!m_recording) return;
|
||||
|
||||
Action action;
|
||||
action.type = ActionType::Wait;
|
||||
action.duration_ms = duration_ms;
|
||||
action.timestamp_ms = GetTimestamp();
|
||||
m_actions.push_back(action);
|
||||
}
|
||||
|
||||
void ActionRecorder::SaveToFile(const std::string& path) const {
|
||||
nlohmann::json j;
|
||||
j["actions"] = nlohmann::json::array();
|
||||
|
||||
for (const auto& action : m_actions) {
|
||||
j["actions"].push_back(action.ToJson());
|
||||
}
|
||||
|
||||
std::ofstream file(path);
|
||||
if (file) {
|
||||
file << j.dump(2);
|
||||
std::cout << "Saved " << m_actions.size() << " actions to " << path << std::endl;
|
||||
} else {
|
||||
std::cerr << "Failed to save actions to " << path << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
void ActionRecorder::LoadFromFile(const std::string& path) {
|
||||
std::ifstream file(path);
|
||||
if (!file) {
|
||||
std::cerr << "Failed to load actions from " << path << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
nlohmann::json j;
|
||||
file >> j;
|
||||
|
||||
m_actions.clear();
|
||||
for (const auto& action_json : j["actions"]) {
|
||||
m_actions.push_back(Action::FromJson(action_json));
|
||||
}
|
||||
|
||||
std::cout << "Loaded " << m_actions.size() << " actions from " << path << std::endl;
|
||||
}
|
||||
|
||||
void ActionRecorder::Clear() {
|
||||
m_actions.clear();
|
||||
}
|
||||
|
||||
int64_t ActionRecorder::GetTimestamp() const {
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(now - m_start_time);
|
||||
return duration.count();
|
||||
}
|
||||
|
||||
} // namespace mosis::testing
|
||||
69
designer/src/testing/action_recorder.h
Normal file
69
designer/src/testing/action_recorder.h
Normal file
@@ -0,0 +1,69 @@
|
||||
// Action recorder for UI testing automation
|
||||
#pragma once
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <chrono>
|
||||
|
||||
namespace mosis::testing {
|
||||
|
||||
// Action types for recording user interactions
|
||||
enum class ActionType {
|
||||
Tap,
|
||||
Swipe,
|
||||
LongPress,
|
||||
Button,
|
||||
Wait
|
||||
};
|
||||
|
||||
// Represents a single recorded action
|
||||
struct Action {
|
||||
ActionType type;
|
||||
double x = 0, y = 0; // For tap, long_press
|
||||
double x1 = 0, y1 = 0; // For swipe start
|
||||
double x2 = 0, y2 = 0; // For swipe end
|
||||
int duration_ms = 0; // For swipe, long_press, wait
|
||||
std::string button; // For button actions (back, home, recents)
|
||||
int64_t timestamp_ms = 0; // Time offset from recording start
|
||||
|
||||
nlohmann::json ToJson() const;
|
||||
static Action FromJson(const nlohmann::json& j);
|
||||
};
|
||||
|
||||
// Records user interactions into a sequence of actions
|
||||
class ActionRecorder {
|
||||
public:
|
||||
ActionRecorder() = default;
|
||||
|
||||
// Start/stop recording
|
||||
void StartRecording();
|
||||
void StopRecording();
|
||||
bool IsRecording() const { return m_recording; }
|
||||
|
||||
// Record individual actions
|
||||
void RecordTap(double x, double y);
|
||||
void RecordSwipe(double x1, double y1, double x2, double y2, int duration_ms);
|
||||
void RecordLongPress(double x, double y, int duration_ms);
|
||||
void RecordButton(const std::string& button);
|
||||
void RecordWait(int duration_ms);
|
||||
|
||||
// Get recorded actions
|
||||
const std::vector<Action>& GetActions() const { return m_actions; }
|
||||
|
||||
// Save/load to JSON
|
||||
void SaveToFile(const std::string& path) const;
|
||||
void LoadFromFile(const std::string& path);
|
||||
|
||||
// Clear recorded actions
|
||||
void Clear();
|
||||
|
||||
private:
|
||||
int64_t GetTimestamp() const;
|
||||
|
||||
bool m_recording = false;
|
||||
std::vector<Action> m_actions;
|
||||
std::chrono::steady_clock::time_point m_start_time;
|
||||
};
|
||||
|
||||
} // namespace mosis::testing
|
||||
183
designer/src/testing/ui_inspector.cpp
Normal file
183
designer/src/testing/ui_inspector.cpp
Normal file
@@ -0,0 +1,183 @@
|
||||
// UI Inspector implementation
|
||||
#include "ui_inspector.h"
|
||||
#include <RmlUi/Core.h>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <filesystem>
|
||||
|
||||
namespace mosis::testing {
|
||||
|
||||
nlohmann::json UIInspector::DumpDocument(Rml::ElementDocument* document) const {
|
||||
nlohmann::json j;
|
||||
|
||||
if (!document) {
|
||||
return j;
|
||||
}
|
||||
|
||||
// Add metadata
|
||||
j["timestamp"] = std::time(nullptr);
|
||||
j["screen"] = document->GetSourceURL();
|
||||
j["resolution"] = {
|
||||
{"width", document->GetContext()->GetDimensions().x},
|
||||
{"height", document->GetContext()->GetDimensions().y}
|
||||
};
|
||||
|
||||
// Dump element tree
|
||||
j["elements"] = DumpElement(document);
|
||||
|
||||
return j;
|
||||
}
|
||||
|
||||
nlohmann::json UIInspector::DumpElement(Rml::Element* element) const {
|
||||
nlohmann::json j;
|
||||
|
||||
if (!element) {
|
||||
return j;
|
||||
}
|
||||
|
||||
j["tag"] = element->GetTagName();
|
||||
j["id"] = element->GetId();
|
||||
|
||||
// Get classes
|
||||
nlohmann::json classes = nlohmann::json::array();
|
||||
Rml::String class_attr = element->GetAttribute<Rml::String>("class", "");
|
||||
if (!class_attr.empty()) {
|
||||
// Split by space
|
||||
size_t start = 0;
|
||||
size_t end;
|
||||
while ((end = class_attr.find(' ', start)) != std::string::npos) {
|
||||
if (end > start) {
|
||||
classes.push_back(class_attr.substr(start, end - start));
|
||||
}
|
||||
start = end + 1;
|
||||
}
|
||||
if (start < class_attr.size()) {
|
||||
classes.push_back(class_attr.substr(start));
|
||||
}
|
||||
}
|
||||
j["classes"] = classes;
|
||||
|
||||
// Get bounds
|
||||
auto bounds = GetBounds(element);
|
||||
j["bounds"] = bounds.ToJson();
|
||||
|
||||
// Visibility
|
||||
j["visible"] = IsVisible(element);
|
||||
|
||||
// Children
|
||||
nlohmann::json children = nlohmann::json::array();
|
||||
for (int i = 0; i < element->GetNumChildren(); ++i) {
|
||||
Rml::Element* child = element->GetChild(i);
|
||||
if (child && child->GetTagName() != "#text") {
|
||||
children.push_back(DumpElement(child));
|
||||
}
|
||||
}
|
||||
|
||||
// Text content (only for leaf elements without children to avoid huge JSON)
|
||||
if (children.empty()) {
|
||||
std::string text = GetText(element);
|
||||
// Only store short text to avoid huge JSON and escaping issues
|
||||
if (!text.empty() && text.length() < 200) {
|
||||
j["text"] = text;
|
||||
} else {
|
||||
j["text"] = nlohmann::json(); // null
|
||||
}
|
||||
} else {
|
||||
j["text"] = nlohmann::json(); // null for non-leaf elements
|
||||
j["children"] = children;
|
||||
}
|
||||
|
||||
return j;
|
||||
}
|
||||
|
||||
Rml::Element* UIInspector::FindById(Rml::ElementDocument* document, const std::string& id) const {
|
||||
if (!document) return nullptr;
|
||||
return document->GetElementById(id);
|
||||
}
|
||||
|
||||
std::vector<Rml::Element*> UIInspector::FindByClass(Rml::ElementDocument* document, const std::string& class_name) const {
|
||||
std::vector<Rml::Element*> results;
|
||||
|
||||
if (!document) return results;
|
||||
|
||||
Rml::ElementList elements;
|
||||
document->GetElementsByClassName(elements, class_name);
|
||||
|
||||
for (auto* element : elements) {
|
||||
results.push_back(element);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
bool UIInspector::IsVisible(Rml::Element* element) const {
|
||||
if (!element) return false;
|
||||
|
||||
// Check if element has zero size
|
||||
auto box = element->GetBox();
|
||||
if (box.GetSize().x <= 0 || box.GetSize().y <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check display property
|
||||
auto display = element->GetProperty<Rml::Style::Display>("display");
|
||||
if (display == Rml::Style::Display::None) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check visibility property
|
||||
auto visibility = element->GetProperty<Rml::Style::Visibility>("visibility");
|
||||
if (visibility == Rml::Style::Visibility::Hidden) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
ElementBounds UIInspector::GetBounds(Rml::Element* element) const {
|
||||
ElementBounds bounds = {0, 0, 0, 0};
|
||||
|
||||
if (!element) return bounds;
|
||||
|
||||
auto abs_offset = element->GetAbsoluteOffset(Rml::BoxArea::Border);
|
||||
auto box = element->GetBox();
|
||||
|
||||
bounds.x = abs_offset.x;
|
||||
bounds.y = abs_offset.y;
|
||||
bounds.width = box.GetSize(Rml::BoxArea::Border).x;
|
||||
bounds.height = box.GetSize(Rml::BoxArea::Border).y;
|
||||
|
||||
return bounds;
|
||||
}
|
||||
|
||||
std::string UIInspector::GetText(Rml::Element* element) const {
|
||||
if (!element) return "";
|
||||
return element->GetInnerRML();
|
||||
}
|
||||
|
||||
void UIInspector::SaveToFile(Rml::ElementDocument* document, const std::string& path, bool quiet) const {
|
||||
nlohmann::json j = DumpDocument(document);
|
||||
|
||||
// Write to temp file first, then rename for atomic update
|
||||
std::string tempPath = path + ".tmp";
|
||||
std::ofstream file(tempPath);
|
||||
if (file) {
|
||||
file << j.dump(2);
|
||||
file.close(); // Ensure file is closed and flushed
|
||||
|
||||
// Atomic rename (on Windows, need to remove destination first)
|
||||
std::error_code ec;
|
||||
std::filesystem::remove(path, ec);
|
||||
std::filesystem::rename(tempPath, path, ec);
|
||||
|
||||
if (ec && !quiet) {
|
||||
std::cerr << "Failed to rename temp file: " << ec.message() << std::endl;
|
||||
} else if (!quiet) {
|
||||
std::cout << "Saved UI hierarchy to " << path << std::endl;
|
||||
}
|
||||
} else {
|
||||
std::cerr << "Failed to save UI hierarchy to " << path << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace mosis::testing
|
||||
53
designer/src/testing/ui_inspector.h
Normal file
53
designer/src/testing/ui_inspector.h
Normal file
@@ -0,0 +1,53 @@
|
||||
// UI Inspector for dumping element hierarchy
|
||||
#pragma once
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
|
||||
namespace Rml {
|
||||
class Element;
|
||||
class ElementDocument;
|
||||
}
|
||||
|
||||
namespace mosis::testing {
|
||||
|
||||
// Element bounds
|
||||
struct ElementBounds {
|
||||
float x, y, width, height;
|
||||
|
||||
nlohmann::json ToJson() const {
|
||||
return {{"x", x}, {"y", y}, {"width", width}, {"height", height}};
|
||||
}
|
||||
};
|
||||
|
||||
// Inspects and dumps UI element hierarchy
|
||||
class UIInspector {
|
||||
public:
|
||||
UIInspector() = default;
|
||||
|
||||
// Dump the element tree of a document to JSON
|
||||
nlohmann::json DumpDocument(Rml::ElementDocument* document) const;
|
||||
|
||||
// Dump a single element and its children
|
||||
nlohmann::json DumpElement(Rml::Element* element) const;
|
||||
|
||||
// Find element by ID
|
||||
Rml::Element* FindById(Rml::ElementDocument* document, const std::string& id) const;
|
||||
|
||||
// Find elements by class
|
||||
std::vector<Rml::Element*> FindByClass(Rml::ElementDocument* document, const std::string& class_name) const;
|
||||
|
||||
// Check if element is visible
|
||||
bool IsVisible(Rml::Element* element) const;
|
||||
|
||||
// Get element bounds (in screen coordinates)
|
||||
ElementBounds GetBounds(Rml::Element* element) const;
|
||||
|
||||
// Get element text content
|
||||
std::string GetText(Rml::Element* element) const;
|
||||
|
||||
// Save hierarchy to file (quiet=true suppresses log message)
|
||||
void SaveToFile(Rml::ElementDocument* document, const std::string& path, bool quiet = false) const;
|
||||
};
|
||||
|
||||
} // namespace mosis::testing
|
||||
244
designer/src/testing/visual_capture.cpp
Normal file
244
designer/src/testing/visual_capture.cpp
Normal file
@@ -0,0 +1,244 @@
|
||||
// Visual capture implementation
|
||||
#include "visual_capture.h"
|
||||
#include <png.h>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
|
||||
// OpenGL header (from RmlUi backend)
|
||||
#include <RmlUi_Include_GL3.h>
|
||||
|
||||
namespace mosis::testing {
|
||||
|
||||
ImageData VisualCapture::CaptureFramebuffer(uint32_t width, uint32_t height) const {
|
||||
ImageData image;
|
||||
image.width = width;
|
||||
image.height = height;
|
||||
image.pixels.resize(width * height * 4);
|
||||
|
||||
glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, image.pixels.data());
|
||||
|
||||
// Flip vertically (OpenGL origin is bottom-left)
|
||||
FlipVertically(image);
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
bool VisualCapture::SavePNG(const ImageData& image, const std::string& path) const {
|
||||
if (!image.IsValid()) {
|
||||
std::cerr << "Invalid image data" << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
FILE* fp = fopen(path.c_str(), "wb");
|
||||
if (!fp) {
|
||||
std::cerr << "Failed to open file for writing: " << path << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
png_structp png = png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
|
||||
if (!png) {
|
||||
fclose(fp);
|
||||
return false;
|
||||
}
|
||||
|
||||
png_infop info = png_create_info_struct(png);
|
||||
if (!info) {
|
||||
png_destroy_write_struct(&png, nullptr);
|
||||
fclose(fp);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (setjmp(png_jmpbuf(png))) {
|
||||
png_destroy_write_struct(&png, &info);
|
||||
fclose(fp);
|
||||
return false;
|
||||
}
|
||||
|
||||
png_init_io(png, fp);
|
||||
|
||||
png_set_IHDR(png, info, image.width, image.height, 8,
|
||||
PNG_COLOR_TYPE_RGBA, PNG_INTERLACE_NONE,
|
||||
PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);
|
||||
|
||||
png_write_info(png, info);
|
||||
|
||||
// Write rows
|
||||
std::vector<png_bytep> rows(image.height);
|
||||
for (uint32_t y = 0; y < image.height; ++y) {
|
||||
rows[y] = const_cast<png_bytep>(image.pixels.data() + y * image.width * 4);
|
||||
}
|
||||
|
||||
png_write_image(png, rows.data());
|
||||
png_write_end(png, nullptr);
|
||||
|
||||
png_destroy_write_struct(&png, &info);
|
||||
fclose(fp);
|
||||
|
||||
std::cout << "Saved screenshot to " << path << std::endl;
|
||||
return true;
|
||||
}
|
||||
|
||||
ImageData VisualCapture::LoadPNG(const std::string& path) const {
|
||||
ImageData image;
|
||||
|
||||
FILE* fp = fopen(path.c_str(), "rb");
|
||||
if (!fp) {
|
||||
std::cerr << "Failed to open file for reading: " << path << std::endl;
|
||||
return image;
|
||||
}
|
||||
|
||||
png_structp png = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
|
||||
if (!png) {
|
||||
fclose(fp);
|
||||
return image;
|
||||
}
|
||||
|
||||
png_infop info = png_create_info_struct(png);
|
||||
if (!info) {
|
||||
png_destroy_read_struct(&png, nullptr, nullptr);
|
||||
fclose(fp);
|
||||
return image;
|
||||
}
|
||||
|
||||
if (setjmp(png_jmpbuf(png))) {
|
||||
png_destroy_read_struct(&png, &info, nullptr);
|
||||
fclose(fp);
|
||||
return image;
|
||||
}
|
||||
|
||||
png_init_io(png, fp);
|
||||
png_read_info(png, info);
|
||||
|
||||
image.width = png_get_image_width(png, info);
|
||||
image.height = png_get_image_height(png, info);
|
||||
png_byte color_type = png_get_color_type(png, info);
|
||||
png_byte bit_depth = png_get_bit_depth(png, info);
|
||||
|
||||
// Convert to RGBA
|
||||
if (bit_depth == 16) {
|
||||
png_set_strip_16(png);
|
||||
}
|
||||
if (color_type == PNG_COLOR_TYPE_PALETTE) {
|
||||
png_set_palette_to_rgb(png);
|
||||
}
|
||||
if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8) {
|
||||
png_set_expand_gray_1_2_4_to_8(png);
|
||||
}
|
||||
if (png_get_valid(png, info, PNG_INFO_tRNS)) {
|
||||
png_set_tRNS_to_alpha(png);
|
||||
}
|
||||
if (color_type == PNG_COLOR_TYPE_RGB || color_type == PNG_COLOR_TYPE_GRAY || color_type == PNG_COLOR_TYPE_PALETTE) {
|
||||
png_set_filler(png, 0xFF, PNG_FILLER_AFTER);
|
||||
}
|
||||
if (color_type == PNG_COLOR_TYPE_GRAY || color_type == PNG_COLOR_TYPE_GRAY_ALPHA) {
|
||||
png_set_gray_to_rgb(png);
|
||||
}
|
||||
|
||||
png_read_update_info(png, info);
|
||||
|
||||
// Read rows
|
||||
image.pixels.resize(image.width * image.height * 4);
|
||||
std::vector<png_bytep> rows(image.height);
|
||||
for (uint32_t y = 0; y < image.height; ++y) {
|
||||
rows[y] = image.pixels.data() + y * image.width * 4;
|
||||
}
|
||||
|
||||
png_read_image(png, rows.data());
|
||||
|
||||
png_destroy_read_struct(&png, &info, nullptr);
|
||||
fclose(fp);
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
CompareResult VisualCapture::Compare(const ImageData& actual, const ImageData& expected, double threshold) const {
|
||||
CompareResult result;
|
||||
|
||||
if (!actual.IsValid() || !expected.IsValid()) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (actual.width != expected.width || actual.height != expected.height) {
|
||||
std::cerr << "Image dimensions don't match" << std::endl;
|
||||
return result;
|
||||
}
|
||||
|
||||
uint32_t total_pixels = actual.width * actual.height;
|
||||
uint32_t diff_pixels = 0;
|
||||
|
||||
for (uint32_t i = 0; i < actual.pixels.size(); i += 4) {
|
||||
int dr = std::abs(static_cast<int>(actual.pixels[i]) - static_cast<int>(expected.pixels[i]));
|
||||
int dg = std::abs(static_cast<int>(actual.pixels[i+1]) - static_cast<int>(expected.pixels[i+1]));
|
||||
int db = std::abs(static_cast<int>(actual.pixels[i+2]) - static_cast<int>(expected.pixels[i+2]));
|
||||
int da = std::abs(static_cast<int>(actual.pixels[i+3]) - static_cast<int>(expected.pixels[i+3]));
|
||||
|
||||
// If any channel differs significantly, count as different
|
||||
if (dr > 2 || dg > 2 || db > 2 || da > 2) {
|
||||
++diff_pixels;
|
||||
}
|
||||
}
|
||||
|
||||
result.diff_pixels = diff_pixels;
|
||||
result.diff_percent = static_cast<double>(diff_pixels) / total_pixels;
|
||||
result.match = result.diff_percent <= threshold;
|
||||
|
||||
if (!result.match) {
|
||||
result.diff_image = GenerateDiff(actual, expected);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
ImageData VisualCapture::GenerateDiff(const ImageData& actual, const ImageData& expected) const {
|
||||
ImageData diff;
|
||||
|
||||
if (!actual.IsValid() || !expected.IsValid()) {
|
||||
return diff;
|
||||
}
|
||||
|
||||
diff.width = actual.width;
|
||||
diff.height = actual.height;
|
||||
diff.pixels.resize(actual.pixels.size());
|
||||
|
||||
for (uint32_t i = 0; i < actual.pixels.size(); i += 4) {
|
||||
int dr = std::abs(static_cast<int>(actual.pixels[i]) - static_cast<int>(expected.pixels[i]));
|
||||
int dg = std::abs(static_cast<int>(actual.pixels[i+1]) - static_cast<int>(expected.pixels[i+1]));
|
||||
int db = std::abs(static_cast<int>(actual.pixels[i+2]) - static_cast<int>(expected.pixels[i+2]));
|
||||
|
||||
if (dr > 2 || dg > 2 || db > 2) {
|
||||
// Highlight difference in red
|
||||
diff.pixels[i] = 255;
|
||||
diff.pixels[i+1] = 0;
|
||||
diff.pixels[i+2] = 0;
|
||||
diff.pixels[i+3] = 255;
|
||||
} else {
|
||||
// Dim the matching pixels
|
||||
diff.pixels[i] = actual.pixels[i] / 3;
|
||||
diff.pixels[i+1] = actual.pixels[i+1] / 3;
|
||||
diff.pixels[i+2] = actual.pixels[i+2] / 3;
|
||||
diff.pixels[i+3] = 255;
|
||||
}
|
||||
}
|
||||
|
||||
return diff;
|
||||
}
|
||||
|
||||
void VisualCapture::FlipVertically(ImageData& image) const {
|
||||
if (!image.IsValid()) return;
|
||||
|
||||
size_t row_size = image.width * 4;
|
||||
std::vector<uint8_t> temp_row(row_size);
|
||||
|
||||
for (uint32_t y = 0; y < image.height / 2; ++y) {
|
||||
uint8_t* top = image.pixels.data() + y * row_size;
|
||||
uint8_t* bottom = image.pixels.data() + (image.height - 1 - y) * row_size;
|
||||
|
||||
memcpy(temp_row.data(), top, row_size);
|
||||
memcpy(top, bottom, row_size);
|
||||
memcpy(bottom, temp_row.data(), row_size);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace mosis::testing
|
||||
51
designer/src/testing/visual_capture.h
Normal file
51
designer/src/testing/visual_capture.h
Normal file
@@ -0,0 +1,51 @@
|
||||
// Visual capture for screenshots and image comparison
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
|
||||
namespace mosis::testing {
|
||||
|
||||
// PNG image data
|
||||
struct ImageData {
|
||||
uint32_t width = 0;
|
||||
uint32_t height = 0;
|
||||
std::vector<uint8_t> pixels; // RGBA format
|
||||
|
||||
bool IsValid() const { return width > 0 && height > 0 && !pixels.empty(); }
|
||||
};
|
||||
|
||||
// Result of image comparison
|
||||
struct CompareResult {
|
||||
bool match = false;
|
||||
double diff_percent = 0.0;
|
||||
uint32_t diff_pixels = 0;
|
||||
ImageData diff_image;
|
||||
};
|
||||
|
||||
// Captures and compares screenshots
|
||||
class VisualCapture {
|
||||
public:
|
||||
VisualCapture() = default;
|
||||
|
||||
// Capture current framebuffer to image
|
||||
ImageData CaptureFramebuffer(uint32_t width, uint32_t height) const;
|
||||
|
||||
// Save image to PNG file
|
||||
bool SavePNG(const ImageData& image, const std::string& path) const;
|
||||
|
||||
// Load PNG file to image
|
||||
ImageData LoadPNG(const std::string& path) const;
|
||||
|
||||
// Compare two images
|
||||
CompareResult Compare(const ImageData& actual, const ImageData& expected, double threshold = 0.01) const;
|
||||
|
||||
// Generate diff image (highlights differences)
|
||||
ImageData GenerateDiff(const ImageData& actual, const ImageData& expected) const;
|
||||
|
||||
// Utility: flip image vertically (for OpenGL coordinate conversion)
|
||||
void FlipVertically(ImageData& image) const;
|
||||
};
|
||||
|
||||
} // namespace mosis::testing
|
||||
144
designer/test/README.md
Normal file
144
designer/test/README.md
Normal file
@@ -0,0 +1,144 @@
|
||||
# Mosis Designer UI Testing with AutoHotkey
|
||||
|
||||
## Overview
|
||||
|
||||
This folder contains AutoHotkey v2 scripts for automated UI testing of the Mosis Designer.
|
||||
|
||||
## Testing Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Test Runner (run_tests.ahk) │
|
||||
│ - Launches designer process │
|
||||
│ - Captures stdout for verification │
|
||||
│ - Runs test scripts sequentially │
|
||||
│ - Generates test report │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Test Scripts (test_*.ahk) │
|
||||
│ - test_navigation.ahk: Click apps, verify screen change │
|
||||
│ - test_back_button.ahk: Navigate and go back │
|
||||
│ - test_home_button.ahk: Navigate deep, press home │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Utilities (lib/utils.ahk) │
|
||||
│ - FindDesignerWindow(): Get window handle │
|
||||
│ - ClickPhone(x, y): Click at phone coordinates │
|
||||
│ - WaitForScreen(name): Wait for navigation log │
|
||||
│ - CaptureScreenshot(path): Save window image │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Window Identification
|
||||
|
||||
- **Window Title**: "Mosis Designer"
|
||||
- **Window Class**: GLFW window class (varies by version)
|
||||
- **Size**: 540x960 client area (phone resolution)
|
||||
|
||||
## Coordinate System
|
||||
|
||||
The phone UI uses a 540x960 coordinate system. AHK needs to:
|
||||
|
||||
1. Find the designer window
|
||||
2. Get the client area position
|
||||
3. Convert phone coordinates to screen coordinates
|
||||
|
||||
```
|
||||
Phone Coordinates (0,0 to 540,960)
|
||||
│
|
||||
▼
|
||||
┌──────────────────┐
|
||||
│ Window Title │ ← Window decorations
|
||||
├──────────────────┤
|
||||
│ │
|
||||
│ Phone UI │ ← Client area (540x960)
|
||||
│ (0,0) │
|
||||
│ ┌──────┐ │
|
||||
│ │ App │ │ ← Click target
|
||||
│ │ Icon │ │
|
||||
│ └──────┘ │
|
||||
│ │
|
||||
│ (540,960)
|
||||
└──────────────────┘
|
||||
```
|
||||
|
||||
## App Icon Positions (Home Screen)
|
||||
|
||||
Based on the home.rml layout with 4 columns:
|
||||
|
||||
| Row | App | Approx X | Approx Y |
|
||||
|-----|-----------|----------|----------|
|
||||
| 1 | Phone | 67 | 100 |
|
||||
| 1 | Messages | 202 | 100 |
|
||||
| 1 | Contacts | 337 | 100 |
|
||||
| 1 | Browser | 472 | 100 |
|
||||
| 2 | Gallery | 67 | 200 |
|
||||
| 2 | Camera | 202 | 200 |
|
||||
| 2 | Settings | 337 | 200 |
|
||||
| 2 | Music | 472 | 200 |
|
||||
|
||||
Dock items at bottom (~900 Y):
|
||||
- Phone: 67
|
||||
- Messages: 202
|
||||
- Contacts: 337
|
||||
- Browser: 472
|
||||
|
||||
## Verification Methods
|
||||
|
||||
### 1. Console Output Parsing
|
||||
The designer outputs navigation events to stdout:
|
||||
```
|
||||
navigateTo called with: dialer
|
||||
Loaded screen: apps/dialer/dialer.rml
|
||||
Navigated to: dialer (history depth: 1)
|
||||
goBack called (history depth: 1)
|
||||
Back to: home
|
||||
```
|
||||
|
||||
We can redirect stdout to a file and parse it for verification.
|
||||
|
||||
### 2. Screenshot Comparison
|
||||
Take screenshots at key points and compare with baselines.
|
||||
|
||||
### 3. Window Title Changes (future)
|
||||
Could modify designer to include current screen in title.
|
||||
|
||||
## Test Execution Flow
|
||||
|
||||
```
|
||||
1. Start designer with stdout redirected to log file
|
||||
2. Wait for window to appear
|
||||
3. Wait for "Document loaded successfully" in log
|
||||
4. Execute test actions (clicks, waits)
|
||||
5. Parse log for expected navigation events
|
||||
6. Report pass/fail
|
||||
7. Close designer
|
||||
```
|
||||
|
||||
## Files
|
||||
|
||||
- `lib/utils.ahk` - Shared utility functions
|
||||
- `run_tests.ahk` - Main test runner
|
||||
- `test_navigation.ahk` - Navigation test suite
|
||||
- `test_back_button.ahk` - Back button test suite
|
||||
- `config.ahk` - Test configuration (paths, timeouts)
|
||||
|
||||
## Usage
|
||||
|
||||
```powershell
|
||||
# Run all tests
|
||||
& "C:\Program Files\AutoHotkey\v2\AutoHotkey64.exe" run_tests.ahk
|
||||
|
||||
# Run specific test
|
||||
& "C:\Program Files\AutoHotkey\v2\AutoHotkey64.exe" test_navigation.ahk
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
- AutoHotkey v2 (installed at C:\Program Files\AutoHotkey\v2)
|
||||
- Mosis Designer built (designer\build\Debug\mosis-designer.exe)
|
||||
- Test assets in place (src\main\assets\)
|
||||
102
designer/test/click_test.ahk
Normal file
102
designer/test/click_test.ahk
Normal file
@@ -0,0 +1,102 @@
|
||||
; Click test for Mosis Designer
|
||||
#Requires AutoHotkey v2.0
|
||||
|
||||
; Configuration
|
||||
WINDOW_TITLE := "Mosis Designer"
|
||||
PHONE_WIDTH := 540
|
||||
PHONE_HEIGHT := 960
|
||||
|
||||
; Output file
|
||||
outFile := A_ScriptDir . "\click_test_result.txt"
|
||||
if (FileExist(outFile))
|
||||
FileDelete(outFile)
|
||||
|
||||
Log(msg) {
|
||||
global outFile
|
||||
FileAppend(msg . "`n", outFile, "UTF-8")
|
||||
}
|
||||
|
||||
Log("=== Click Test ===")
|
||||
Log("Time: " . FormatTime(, "yyyy-MM-dd HH:mm:ss"))
|
||||
|
||||
; Find window
|
||||
hwnd := WinExist(WINDOW_TITLE)
|
||||
if (!hwnd) {
|
||||
Log("ERROR: Window not found")
|
||||
ExitApp(1)
|
||||
}
|
||||
|
||||
Log("Window found: " . hwnd)
|
||||
|
||||
; Get window info
|
||||
WinGetPos(&winX, &winY, &winW, &winH, hwnd)
|
||||
Log("Window position: X=" . winX . " Y=" . winY)
|
||||
Log("Window size: W=" . winW . " H=" . winH)
|
||||
|
||||
; Calculate DPI scale factor
|
||||
; Expected phone size is 540x960
|
||||
; Window has some decorations, estimate ~16 pixels width, ~40 height for title+borders
|
||||
expectedW := 540 + 16
|
||||
expectedH := 960 + 40
|
||||
scaleX := winW / expectedW
|
||||
scaleY := winH / expectedH
|
||||
Log("Estimated scale X: " . scaleX . " Y: " . scaleY)
|
||||
|
||||
; Use average scale (should be similar for both)
|
||||
scale := (scaleX + scaleY) / 2
|
||||
Log("Using scale factor: " . scale)
|
||||
|
||||
; Calculate client area offset (estimate title bar and borders scaled)
|
||||
titleBarHeight := Round(31 * scale)
|
||||
borderWidth := Round(8 * scale)
|
||||
Log("Estimated title bar: " . titleBarHeight . "px, border: " . borderWidth . "px")
|
||||
|
||||
; Client area start
|
||||
clientX := winX + borderWidth
|
||||
clientY := winY + titleBarHeight
|
||||
Log("Client area start: X=" . clientX . " Y=" . clientY)
|
||||
|
||||
; Function to convert phone coords to screen coords
|
||||
PhoneToScreen(phoneX, phoneY) {
|
||||
global clientX, clientY, scale
|
||||
return {
|
||||
x: Round(clientX + phoneX * scale),
|
||||
y: Round(clientY + phoneY * scale)
|
||||
}
|
||||
}
|
||||
|
||||
; Activate window
|
||||
WinActivate(hwnd)
|
||||
Sleep(200)
|
||||
|
||||
Log("")
|
||||
Log("=== Performing Click Test ===")
|
||||
|
||||
; Click on Phone app icon (first app in grid)
|
||||
; Position from home.rml layout: approximately (67, 120) in phone coords
|
||||
phonePos := PhoneToScreen(67, 120)
|
||||
Log("Phone app: phone(67,120) -> screen(" . phonePos.x . "," . phonePos.y . ")")
|
||||
|
||||
; Perform click
|
||||
Click(phonePos.x, phonePos.y)
|
||||
Log("Click sent!")
|
||||
|
||||
Sleep(1000)
|
||||
Log("Test complete - check if screen changed to Dialer")
|
||||
|
||||
; Also try clicking messages in dock for comparison
|
||||
Log("")
|
||||
Log("=== Second Click Test (Messages Dock) ===")
|
||||
; Dock is at bottom, approximately y=920 in phone coords
|
||||
; Messages is second item, approximately x=202
|
||||
msgPos := PhoneToScreen(202, 920)
|
||||
Log("Messages dock: phone(202,920) -> screen(" . msgPos.x . "," . msgPos.y . ")")
|
||||
|
||||
Click(msgPos.x, msgPos.y)
|
||||
Log("Click sent!")
|
||||
|
||||
Sleep(500)
|
||||
Log("")
|
||||
Log("=== Test Complete ===")
|
||||
|
||||
ExitApp(0)
|
||||
45
designer/test/config.ahk
Normal file
45
designer/test/config.ahk
Normal file
@@ -0,0 +1,45 @@
|
||||
; Mosis Designer Test Configuration
|
||||
; AutoHotkey v2
|
||||
|
||||
; Paths
|
||||
global DESIGNER_EXE := A_ScriptDir . "\..\build\Debug\mosis-designer.exe"
|
||||
global ASSETS_PATH := A_ScriptDir . "\..\..\src\main\assets"
|
||||
global HOME_RML := ASSETS_PATH . "\apps\home\home.rml"
|
||||
global LOG_FILE := A_ScriptDir . "\test_output.log"
|
||||
global SCREENSHOT_DIR := A_ScriptDir . "\screenshots"
|
||||
|
||||
; Window settings
|
||||
global WINDOW_TITLE := "Mosis Designer"
|
||||
global PHONE_WIDTH := 540
|
||||
global PHONE_HEIGHT := 960
|
||||
|
||||
; Timeouts (milliseconds)
|
||||
global STARTUP_TIMEOUT := 10000 ; Wait for window to appear
|
||||
global NAVIGATION_TIMEOUT := 2000 ; Wait for navigation to complete
|
||||
global CLICK_DELAY := 100 ; Delay after click
|
||||
|
||||
; App icon positions on home screen (approximate center of each icon)
|
||||
; Grid is 4 columns, icons are ~135px apart horizontally
|
||||
; First row starts around Y=100
|
||||
global APP_POSITIONS := Map(
|
||||
"phone", {x: 67, y: 120},
|
||||
"messages", {x: 202, y: 120},
|
||||
"contacts", {x: 337, y: 120},
|
||||
"browser", {x: 472, y: 120},
|
||||
"gallery", {x: 67, y: 220},
|
||||
"camera", {x: 202, y: 220},
|
||||
"settings", {x: 337, y: 220},
|
||||
"music", {x: 472, y: 220},
|
||||
"calendar", {x: 67, y: 320},
|
||||
"clock", {x: 202, y: 320},
|
||||
"notes", {x: 337, y: 320},
|
||||
"maps", {x: 472, y: 320}
|
||||
)
|
||||
|
||||
; Dock positions (bottom of screen)
|
||||
global DOCK_POSITIONS := Map(
|
||||
"phone", {x: 67, y: 920},
|
||||
"messages", {x: 202, y: 920},
|
||||
"contacts", {x: 337, y: 920},
|
||||
"browser", {x: 472, y: 920}
|
||||
)
|
||||
124
designer/test/diagnose.ahk
Normal file
124
designer/test/diagnose.ahk
Normal file
@@ -0,0 +1,124 @@
|
||||
; Mosis Designer Diagnostic Script
|
||||
; AutoHotkey v2
|
||||
; Outputs window information without user interaction
|
||||
|
||||
#Requires AutoHotkey v2.0
|
||||
|
||||
; Simple config (inline to avoid include issues)
|
||||
WINDOW_TITLE := "Mosis Designer"
|
||||
PHONE_WIDTH := 540
|
||||
PHONE_HEIGHT := 960
|
||||
|
||||
; Output file
|
||||
outputFile := A_ScriptDir . "\diagnose_output.txt"
|
||||
|
||||
; Clear previous output
|
||||
if (FileExist(outputFile))
|
||||
FileDelete(outputFile)
|
||||
|
||||
; Helper to write output
|
||||
WriteOutput(msg) {
|
||||
FileAppend(msg . "`n", outputFile)
|
||||
}
|
||||
|
||||
WriteOutput("=== Mosis Designer Diagnostic ===")
|
||||
WriteOutput("Time: " . A_Now)
|
||||
WriteOutput("")
|
||||
|
||||
; Find window
|
||||
hwnd := WinExist(WINDOW_TITLE)
|
||||
|
||||
if (!hwnd) {
|
||||
WriteOutput("ERROR: Window '" . WINDOW_TITLE . "' not found")
|
||||
WriteOutput("")
|
||||
WriteOutput("Looking for any GLFW windows...")
|
||||
|
||||
; Try to find any GLFW window
|
||||
windows := WinGetList()
|
||||
for wnd in windows {
|
||||
title := WinGetTitle(wnd)
|
||||
class := WinGetClass(wnd)
|
||||
if (InStr(class, "GLFW") || InStr(title, "Mosis") || InStr(title, "Designer")) {
|
||||
WriteOutput("Found candidate: " . wnd . " - Title: " . title . " - Class: " . class)
|
||||
}
|
||||
}
|
||||
ExitApp()
|
||||
}
|
||||
|
||||
WriteOutput("Window found!")
|
||||
WriteOutput("HWND: " . hwnd)
|
||||
|
||||
; Get window info
|
||||
title := WinGetTitle(hwnd)
|
||||
class := WinGetClass(hwnd)
|
||||
WinGetPos(&winX, &winY, &winW, &winH, hwnd)
|
||||
|
||||
WriteOutput("Title: " . title)
|
||||
WriteOutput("Class: " . class)
|
||||
WriteOutput("Position: X=" . winX . " Y=" . winY)
|
||||
WriteOutput("Size: W=" . winW . " H=" . winH)
|
||||
|
||||
; Estimate client area
|
||||
; For GLFW windows, borders are typically small
|
||||
; Let's try different estimates
|
||||
|
||||
WriteOutput("")
|
||||
WriteOutput("=== Client Area Estimates ===")
|
||||
|
||||
; Method 1: Standard Windows decorations
|
||||
borderW := 8
|
||||
titleH := 31
|
||||
clientX1 := winX + borderW
|
||||
clientY1 := winY + titleH
|
||||
WriteOutput("Estimate 1 (standard border): X=" . clientX1 . " Y=" . clientY1)
|
||||
|
||||
; Method 2: Minimal border (GLFW often uses this)
|
||||
borderW2 := 1
|
||||
titleH2 := 1
|
||||
clientX2 := winX + borderW2
|
||||
clientY2 := winY + titleH2
|
||||
WriteOutput("Estimate 2 (minimal border): X=" . clientX2 . " Y=" . clientY2)
|
||||
|
||||
; Method 3: No border (borderless window)
|
||||
WriteOutput("Estimate 3 (no border): X=" . winX . " Y=" . winY)
|
||||
|
||||
; Calculate expected phone area size vs actual window
|
||||
WriteOutput("")
|
||||
WriteOutput("=== Size Analysis ===")
|
||||
WriteOutput("Expected phone size: " . PHONE_WIDTH . "x" . PHONE_HEIGHT)
|
||||
WriteOutput("Actual window size: " . winW . "x" . winH)
|
||||
|
||||
expectedW := PHONE_WIDTH + (2 * 8) ; borders
|
||||
expectedH := PHONE_HEIGHT + 31 + 8 ; title + border
|
||||
WriteOutput("Expected window size (with decorations): " . expectedW . "x" . expectedH)
|
||||
|
||||
; Check if window is maximized or has different size
|
||||
if (winW > PHONE_WIDTH + 50 || winH > PHONE_HEIGHT + 100) {
|
||||
WriteOutput("WARNING: Window seems larger than expected - may be scaled or maximized")
|
||||
}
|
||||
|
||||
; Try a click test (won't do anything without activation, but log coordinates)
|
||||
WriteOutput("")
|
||||
WriteOutput("=== Click Coordinate Test ===")
|
||||
|
||||
; Phone center
|
||||
phoneX := 270
|
||||
phoneY := 480
|
||||
|
||||
; Convert to screen (using estimate 1)
|
||||
screenX := clientX1 + phoneX
|
||||
screenY := clientY1 + phoneY
|
||||
WriteOutput("Phone center (270, 480) -> Screen (" . screenX . ", " . screenY . ")")
|
||||
|
||||
; Phone app icon position
|
||||
phoneAppX := 67
|
||||
phoneAppY := 120
|
||||
screenAppX := clientX1 + phoneAppX
|
||||
screenAppY := clientY1 + phoneAppY
|
||||
WriteOutput("Phone app icon (67, 120) -> Screen (" . screenAppX . ", " . screenAppY . ")")
|
||||
|
||||
WriteOutput("")
|
||||
WriteOutput("=== Diagnostic Complete ===")
|
||||
WriteOutput("Output written to: " . outputFile)
|
||||
|
||||
ExitApp()
|
||||
149
designer/test/full_click_test.ahk
Normal file
149
designer/test/full_click_test.ahk
Normal file
@@ -0,0 +1,149 @@
|
||||
; Full click test with output verification
|
||||
#Requires AutoHotkey v2.0
|
||||
|
||||
; Configuration
|
||||
DESIGNER_EXE := A_ScriptDir . "\..\build\Debug\mosis-designer.exe"
|
||||
HOME_RML := A_ScriptDir . "\..\..\src\main\assets\apps\home\home.rml"
|
||||
WINDOW_TITLE := "Mosis Designer"
|
||||
PHONE_WIDTH := 540
|
||||
PHONE_HEIGHT := 960
|
||||
|
||||
; Output files
|
||||
logFile := A_ScriptDir . "\designer_output.log"
|
||||
resultFile := A_ScriptDir . "\full_test_result.txt"
|
||||
|
||||
if (FileExist(logFile))
|
||||
FileDelete(logFile)
|
||||
if (FileExist(resultFile))
|
||||
FileDelete(resultFile)
|
||||
|
||||
Log(msg) {
|
||||
global resultFile
|
||||
FileAppend(msg . "`n", resultFile, "UTF-8")
|
||||
}
|
||||
|
||||
Log("=== Full Click Test with Output Capture ===")
|
||||
Log("Time: " . FormatTime(, "yyyy-MM-dd HH:mm:ss"))
|
||||
Log("")
|
||||
|
||||
; Start designer with output redirection
|
||||
Log("Starting designer...")
|
||||
cmd := '"' . DESIGNER_EXE . '" "' . HOME_RML . '" > "' . logFile . '" 2>&1'
|
||||
Log("Command: " . cmd)
|
||||
|
||||
; Run via cmd to handle redirection
|
||||
Run(A_ComSpec . " /c " . cmd, A_ScriptDir, , &pid)
|
||||
Log("Started with PID: " . pid)
|
||||
|
||||
; Wait for window
|
||||
Log("Waiting for window...")
|
||||
startTime := A_TickCount
|
||||
hwnd := 0
|
||||
while (A_TickCount - startTime < 15000) {
|
||||
hwnd := WinExist(WINDOW_TITLE)
|
||||
if (hwnd)
|
||||
break
|
||||
Sleep(200)
|
||||
}
|
||||
|
||||
if (!hwnd) {
|
||||
Log("ERROR: Window did not appear within 15 seconds")
|
||||
ExitApp(1)
|
||||
}
|
||||
Log("Window appeared: " . hwnd)
|
||||
|
||||
; Wait for document to load
|
||||
Sleep(2000)
|
||||
|
||||
; Get window dimensions and calculate scale
|
||||
WinGetPos(&winX, &winY, &winW, &winH, hwnd)
|
||||
Log("Window: X=" . winX . " Y=" . winY . " W=" . winW . " H=" . winH)
|
||||
|
||||
; Calculate scale
|
||||
expectedW := 540 + 16
|
||||
expectedH := 960 + 40
|
||||
scale := (winW / expectedW + winH / expectedH) / 2
|
||||
Log("DPI Scale: " . scale)
|
||||
|
||||
; Calculate client offset
|
||||
titleBar := Round(31 * scale)
|
||||
border := Round(8 * scale)
|
||||
clientX := winX + border
|
||||
clientY := winY + titleBar
|
||||
Log("Client: X=" . clientX . " Y=" . clientY)
|
||||
|
||||
; Activate window
|
||||
WinActivate(hwnd)
|
||||
Sleep(300)
|
||||
|
||||
; Function to click at phone coordinates
|
||||
ClickAt(phoneX, phoneY, desc) {
|
||||
global clientX, clientY, scale, hwnd
|
||||
screenX := Round(clientX + phoneX * scale)
|
||||
screenY := Round(clientY + phoneY * scale)
|
||||
Log("Click " . desc . ": phone(" . phoneX . "," . phoneY . ") -> screen(" . screenX . "," . screenY . ")")
|
||||
|
||||
; Activate and click
|
||||
WinActivate(hwnd)
|
||||
Sleep(50)
|
||||
Click(screenX, screenY)
|
||||
Sleep(500)
|
||||
}
|
||||
|
||||
Log("")
|
||||
Log("=== Test 1: Click Phone App ===")
|
||||
ClickAt(67, 120, "Phone app")
|
||||
Sleep(1500)
|
||||
|
||||
; Check log for navigation
|
||||
Log("Checking log for navigation...")
|
||||
if (FileExist(logFile)) {
|
||||
content := FileRead(logFile)
|
||||
if (InStr(content, "Navigated to: dialer") || InStr(content, "Loaded screen: apps/dialer")) {
|
||||
Log("SUCCESS: Navigation to dialer detected!")
|
||||
} else if (InStr(content, "navigateTo called")) {
|
||||
Log("PARTIAL: navigateTo was called but navigation may have failed")
|
||||
} else {
|
||||
Log("FAIL: No navigation detected")
|
||||
}
|
||||
} else {
|
||||
Log("WARNING: Log file not found")
|
||||
}
|
||||
|
||||
Log("")
|
||||
Log("=== Test 2: Click Messages Dock ===")
|
||||
ClickAt(202, 920, "Messages dock")
|
||||
Sleep(1500)
|
||||
|
||||
if (FileExist(logFile)) {
|
||||
content := FileRead(logFile)
|
||||
if (InStr(content, "messages")) {
|
||||
Log("SUCCESS: Messages-related activity detected")
|
||||
} else {
|
||||
Log("FAIL: No messages navigation detected")
|
||||
}
|
||||
}
|
||||
|
||||
Log("")
|
||||
Log("=== Cleaning Up ===")
|
||||
; Close designer
|
||||
WinClose(hwnd)
|
||||
Sleep(500)
|
||||
|
||||
; Dump the full log
|
||||
Log("")
|
||||
Log("=== Designer Log Contents ===")
|
||||
if (FileExist(logFile)) {
|
||||
content := FileRead(logFile)
|
||||
; Only show last part of log (navigation related)
|
||||
lines := StrSplit(content, "`n")
|
||||
Log("(Last 30 lines)")
|
||||
startIdx := lines.Length > 30 ? lines.Length - 30 : 1
|
||||
loop lines.Length - startIdx + 1 {
|
||||
Log(lines[startIdx + A_Index - 1])
|
||||
}
|
||||
}
|
||||
|
||||
Log("")
|
||||
Log("=== Test Complete ===")
|
||||
ExitApp(0)
|
||||
298
designer/test/lib/utils.ahk
Normal file
298
designer/test/lib/utils.ahk
Normal file
@@ -0,0 +1,298 @@
|
||||
; Mosis Designer Test Utilities
|
||||
; AutoHotkey v2
|
||||
|
||||
#Include "..\config.ahk"
|
||||
|
||||
; Find the designer window and return its handle
|
||||
FindDesignerWindow() {
|
||||
return WinExist(WINDOW_TITLE)
|
||||
}
|
||||
|
||||
; Wait for the designer window to appear
|
||||
; Returns window handle or 0 if timeout
|
||||
WaitForDesignerWindow(timeout := 0) {
|
||||
if (timeout = 0)
|
||||
timeout := STARTUP_TIMEOUT
|
||||
|
||||
startTime := A_TickCount
|
||||
while (A_TickCount - startTime < timeout) {
|
||||
hwnd := FindDesignerWindow()
|
||||
if (hwnd) {
|
||||
; Give window time to fully initialize
|
||||
Sleep(500)
|
||||
return hwnd
|
||||
}
|
||||
Sleep(100)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
; Get the client area position of the designer window
|
||||
; Returns {x, y, w, h} or 0 if window not found
|
||||
GetClientArea(hwnd := 0) {
|
||||
if (hwnd = 0)
|
||||
hwnd := FindDesignerWindow()
|
||||
if (!hwnd)
|
||||
return 0
|
||||
|
||||
; Get window position
|
||||
WinGetPos(&winX, &winY, &winW, &winH, hwnd)
|
||||
|
||||
; Get client area - for GLFW windows, typically minimal border
|
||||
; Client area starts after title bar
|
||||
; We'll estimate based on typical Windows decorations
|
||||
; Title bar is usually ~30-40 pixels, borders ~8 pixels each side
|
||||
|
||||
; For a more accurate approach, we could use DllCall to GetClientRect
|
||||
; But for GLFW windows, the client area often matches the requested size
|
||||
|
||||
; Calculate client area assuming standard decorations
|
||||
borderWidth := 8
|
||||
titleHeight := 31
|
||||
|
||||
return {
|
||||
x: winX + borderWidth,
|
||||
y: winY + titleHeight,
|
||||
w: PHONE_WIDTH,
|
||||
h: PHONE_HEIGHT
|
||||
}
|
||||
}
|
||||
|
||||
; Convert phone coordinates to screen coordinates
|
||||
PhoneToScreen(phoneX, phoneY, hwnd := 0) {
|
||||
client := GetClientArea(hwnd)
|
||||
if (!client)
|
||||
return 0
|
||||
|
||||
return {
|
||||
x: client.x + phoneX,
|
||||
y: client.y + phoneY
|
||||
}
|
||||
}
|
||||
|
||||
; Click at phone coordinates
|
||||
ClickPhone(phoneX, phoneY, hwnd := 0) {
|
||||
if (hwnd = 0)
|
||||
hwnd := FindDesignerWindow()
|
||||
if (!hwnd) {
|
||||
LogMessage("ERROR: Designer window not found")
|
||||
return false
|
||||
}
|
||||
|
||||
; Activate window first
|
||||
WinActivate(hwnd)
|
||||
Sleep(50)
|
||||
|
||||
screen := PhoneToScreen(phoneX, phoneY, hwnd)
|
||||
if (!screen) {
|
||||
LogMessage("ERROR: Could not convert coordinates")
|
||||
return false
|
||||
}
|
||||
|
||||
; Move and click
|
||||
Click(screen.x, screen.y)
|
||||
Sleep(CLICK_DELAY)
|
||||
|
||||
LogMessage("Clicked at phone(" . phoneX . "," . phoneY . ") -> screen(" . screen.x . "," . screen.y . ")")
|
||||
return true
|
||||
}
|
||||
|
||||
; Click on a named app icon
|
||||
ClickApp(appName, hwnd := 0) {
|
||||
if (!APP_POSITIONS.Has(appName)) {
|
||||
LogMessage("ERROR: Unknown app: " . appName)
|
||||
return false
|
||||
}
|
||||
|
||||
pos := APP_POSITIONS[appName]
|
||||
LogMessage("Clicking app: " . appName)
|
||||
return ClickPhone(pos.x, pos.y, hwnd)
|
||||
}
|
||||
|
||||
; Click on a dock item
|
||||
ClickDock(appName, hwnd := 0) {
|
||||
if (!DOCK_POSITIONS.Has(appName)) {
|
||||
LogMessage("ERROR: Unknown dock item: " . appName)
|
||||
return false
|
||||
}
|
||||
|
||||
pos := DOCK_POSITIONS[appName]
|
||||
LogMessage("Clicking dock: " . appName)
|
||||
return ClickPhone(pos.x, pos.y, hwnd)
|
||||
}
|
||||
|
||||
; Send keyboard input to the designer
|
||||
SendToDesigner(keys, hwnd := 0) {
|
||||
if (hwnd = 0)
|
||||
hwnd := FindDesignerWindow()
|
||||
if (!hwnd)
|
||||
return false
|
||||
|
||||
WinActivate(hwnd)
|
||||
Sleep(50)
|
||||
Send(keys)
|
||||
return true
|
||||
}
|
||||
|
||||
; Read the log file and check for a pattern
|
||||
; Returns the matching line or empty string
|
||||
CheckLogFor(pattern, logFile := 0) {
|
||||
if (logFile = 0)
|
||||
logFile := LOG_FILE
|
||||
|
||||
if (!FileExist(logFile))
|
||||
return ""
|
||||
|
||||
content := FileRead(logFile)
|
||||
lines := StrSplit(content, "`n")
|
||||
|
||||
; Search from end (most recent)
|
||||
loop lines.Length {
|
||||
idx := lines.Length - A_Index + 1
|
||||
line := lines[idx]
|
||||
if (InStr(line, pattern))
|
||||
return line
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
; Wait for a specific pattern to appear in the log
|
||||
WaitForLog(pattern, timeout := 0, logFile := 0) {
|
||||
if (timeout = 0)
|
||||
timeout := NAVIGATION_TIMEOUT
|
||||
if (logFile = 0)
|
||||
logFile := LOG_FILE
|
||||
|
||||
startTime := A_TickCount
|
||||
while (A_TickCount - startTime < timeout) {
|
||||
result := CheckLogFor(pattern, logFile)
|
||||
if (result)
|
||||
return result
|
||||
Sleep(100)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
; Wait for navigation to a specific screen
|
||||
WaitForNavigation(screenName, timeout := 0) {
|
||||
pattern := "Navigated to: " . screenName
|
||||
return WaitForLog(pattern, timeout)
|
||||
}
|
||||
|
||||
; Wait for back navigation
|
||||
WaitForBack(timeout := 0) {
|
||||
return WaitForLog("Back to:", timeout)
|
||||
}
|
||||
|
||||
; Take a screenshot of the designer window
|
||||
CaptureScreenshot(filename, hwnd := 0) {
|
||||
if (hwnd = 0)
|
||||
hwnd := FindDesignerWindow()
|
||||
if (!hwnd)
|
||||
return false
|
||||
|
||||
; Ensure screenshot directory exists
|
||||
if (!DirExist(SCREENSHOT_DIR))
|
||||
DirCreate(SCREENSHOT_DIR)
|
||||
|
||||
fullPath := SCREENSHOT_DIR . "\" . filename
|
||||
|
||||
; Use built-in screenshot capability
|
||||
; Note: This captures the entire window including decorations
|
||||
try {
|
||||
; Activate and bring to front
|
||||
WinActivate(hwnd)
|
||||
Sleep(100)
|
||||
|
||||
; Get window position
|
||||
WinGetPos(&x, &y, &w, &h, hwnd)
|
||||
|
||||
; Use GDI+ or Windows API for screenshot
|
||||
; For simplicity, we'll use the Snipping approach
|
||||
; In production, you'd use a proper GDI+ screenshot
|
||||
|
||||
LogMessage("Screenshot requested: " . fullPath . " (not implemented)")
|
||||
return false
|
||||
} catch as e {
|
||||
LogMessage("Screenshot error: " . e.Message)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
; Log a message with timestamp
|
||||
LogMessage(msg) {
|
||||
timestamp := FormatTime(, "yyyy-MM-dd HH:mm:ss")
|
||||
line := "[" . timestamp . "] " . msg . "`n"
|
||||
|
||||
; Write to console
|
||||
OutputDebug(line)
|
||||
|
||||
; Also append to a test log file
|
||||
testLog := A_ScriptDir . "\test_run.log"
|
||||
FileAppend(line, testLog)
|
||||
}
|
||||
|
||||
; Clear test logs
|
||||
ClearLogs() {
|
||||
testLog := A_ScriptDir . "\test_run.log"
|
||||
if (FileExist(testLog))
|
||||
FileDelete(testLog)
|
||||
if (FileExist(LOG_FILE))
|
||||
FileDelete(LOG_FILE)
|
||||
}
|
||||
|
||||
; Close the designer window
|
||||
CloseDesigner(hwnd := 0) {
|
||||
if (hwnd = 0)
|
||||
hwnd := FindDesignerWindow()
|
||||
if (hwnd) {
|
||||
WinClose(hwnd)
|
||||
Sleep(500)
|
||||
}
|
||||
}
|
||||
|
||||
; Kill any running designer processes
|
||||
KillDesigner() {
|
||||
try {
|
||||
Run('taskkill /F /IM mosis-designer.exe', , "Hide")
|
||||
}
|
||||
Sleep(500)
|
||||
}
|
||||
|
||||
; Start the designer process with log redirection
|
||||
; Returns the process ID or 0 on failure
|
||||
StartDesigner(rmlFile := 0) {
|
||||
if (rmlFile = 0)
|
||||
rmlFile := HOME_RML
|
||||
|
||||
; Build command with output redirection
|
||||
cmd := '"' . DESIGNER_EXE . '" "' . rmlFile . '"'
|
||||
|
||||
LogMessage("Starting designer: " . cmd)
|
||||
|
||||
; Start process and capture output
|
||||
; We'll redirect to a log file
|
||||
fullCmd := A_ComSpec . ' /c "' . cmd . '" > "' . LOG_FILE . '" 2>&1'
|
||||
|
||||
Run(fullCmd, A_ScriptDir, "Hide", &pid)
|
||||
|
||||
if (pid) {
|
||||
LogMessage("Designer started with PID: " . pid)
|
||||
return pid
|
||||
}
|
||||
|
||||
LogMessage("ERROR: Failed to start designer")
|
||||
return 0
|
||||
}
|
||||
|
||||
; Assert helper - logs pass/fail and returns result
|
||||
Assert(condition, testName) {
|
||||
if (condition) {
|
||||
LogMessage("PASS: " . testName)
|
||||
return true
|
||||
} else {
|
||||
LogMessage("FAIL: " . testName)
|
||||
return false
|
||||
}
|
||||
}
|
||||
65
designer/test/run_tests.ahk
Normal file
65
designer/test/run_tests.ahk
Normal file
@@ -0,0 +1,65 @@
|
||||
; Mosis Designer Test Runner
|
||||
; AutoHotkey v2
|
||||
; Runs all test scripts and generates a report
|
||||
|
||||
#Requires AutoHotkey v2.0
|
||||
#Include "lib\utils.ahk"
|
||||
|
||||
; Test scripts to run
|
||||
global testScripts := [
|
||||
"test_navigation.ahk"
|
||||
]
|
||||
|
||||
; Run all tests
|
||||
RunAllTests() {
|
||||
LogMessage("========================================")
|
||||
LogMessage("MOSIS DESIGNER TEST SUITE")
|
||||
LogMessage("========================================")
|
||||
|
||||
totalPassed := 0
|
||||
totalFailed := 0
|
||||
|
||||
; Clean up
|
||||
KillDesigner()
|
||||
ClearLogs()
|
||||
|
||||
for script in testScripts {
|
||||
LogMessage("")
|
||||
LogMessage("Running: " . script)
|
||||
LogMessage("----------------------------------------")
|
||||
|
||||
scriptPath := A_ScriptDir . "\" . script
|
||||
|
||||
if (!FileExist(scriptPath)) {
|
||||
LogMessage("ERROR: Script not found: " . scriptPath)
|
||||
totalFailed++
|
||||
continue
|
||||
}
|
||||
|
||||
; Run the test script
|
||||
try {
|
||||
ahkPath := "C:\Program Files\AutoHotkey\v2\AutoHotkey64.exe"
|
||||
RunWait('"' . ahkPath . '" "' . scriptPath . '"', A_ScriptDir)
|
||||
} catch as e {
|
||||
LogMessage("ERROR running script: " . e.Message)
|
||||
totalFailed++
|
||||
}
|
||||
|
||||
; Brief pause between tests
|
||||
Sleep(1000)
|
||||
}
|
||||
|
||||
; Final cleanup
|
||||
KillDesigner()
|
||||
|
||||
; Generate report
|
||||
LogMessage("")
|
||||
LogMessage("========================================")
|
||||
LogMessage("TEST SUITE COMPLETE")
|
||||
LogMessage("Check test_run.log for detailed results")
|
||||
LogMessage("========================================")
|
||||
}
|
||||
|
||||
; Main
|
||||
RunAllTests()
|
||||
ExitApp()
|
||||
21
designer/test/simple_test.ahk
Normal file
21
designer/test/simple_test.ahk
Normal file
@@ -0,0 +1,21 @@
|
||||
; Simple window finder test
|
||||
#Requires AutoHotkey v2.0
|
||||
|
||||
; Write to stdout (will show in console)
|
||||
OutputDebug("Starting simple test`n")
|
||||
|
||||
; Try to find the window
|
||||
hwnd := WinExist("Mosis Designer")
|
||||
|
||||
if (hwnd) {
|
||||
WinGetPos(&x, &y, &w, &h, hwnd)
|
||||
msg := "Found! X:" . x . " Y:" . y . " W:" . w . " H:" . h
|
||||
} else {
|
||||
msg := "Window not found"
|
||||
}
|
||||
|
||||
; Write result to a file
|
||||
outFile := A_ScriptDir . "\simple_result.txt"
|
||||
FileAppend(msg . "`n", outFile, "UTF-8")
|
||||
|
||||
ExitApp()
|
||||
110
designer/test/test_interactive.ahk
Normal file
110
designer/test/test_interactive.ahk
Normal file
@@ -0,0 +1,110 @@
|
||||
; Mosis Designer Interactive Test Helper
|
||||
; AutoHotkey v2
|
||||
; Launches designer and shows coordinates as you click
|
||||
|
||||
#Requires AutoHotkey v2.0
|
||||
#Include "lib\utils.ahk"
|
||||
|
||||
; Global state
|
||||
global isRunning := true
|
||||
global hwnd := 0
|
||||
|
||||
; Create a simple GUI to show status
|
||||
CreateStatusGui() {
|
||||
global statusGui, statusText, coordText
|
||||
|
||||
statusGui := Gui("+AlwaysOnTop", "Mosis Test Helper")
|
||||
statusGui.SetFont("s10")
|
||||
statusGui.Add("Text", "w300", "Designer Status:")
|
||||
statusText := statusGui.Add("Text", "w300 h20", "Not running")
|
||||
statusGui.Add("Text", "w300", "Mouse Position (Phone Coords):")
|
||||
coordText := statusGui.Add("Text", "w300 h20", "N/A")
|
||||
statusGui.Add("Text", "w300", "")
|
||||
statusGui.Add("Button", "w100", "Refresh").OnEvent("Click", RefreshStatus)
|
||||
statusGui.Add("Button", "x+10 w100", "Click Test").OnEvent("Click", DoClickTest)
|
||||
statusGui.Add("Button", "x+10 w80", "Exit").OnEvent("Click", (*) => ExitApp())
|
||||
|
||||
statusGui.OnEvent("Close", (*) => ExitApp())
|
||||
statusGui.Show("x10 y10")
|
||||
}
|
||||
|
||||
; Refresh status display
|
||||
RefreshStatus(*) {
|
||||
global hwnd, statusText
|
||||
|
||||
hwnd := FindDesignerWindow()
|
||||
if (hwnd) {
|
||||
statusText.Value := "Running (hwnd: " . hwnd . ")"
|
||||
} else {
|
||||
statusText.Value := "Not running"
|
||||
}
|
||||
}
|
||||
|
||||
; Perform a test click at center of phone
|
||||
DoClickTest(*) {
|
||||
global hwnd
|
||||
|
||||
hwnd := FindDesignerWindow()
|
||||
if (!hwnd) {
|
||||
MsgBox("Designer window not found!")
|
||||
return
|
||||
}
|
||||
|
||||
; Click center of phone
|
||||
ClickPhone(270, 480, hwnd)
|
||||
MsgBox("Clicked at phone center (270, 480)")
|
||||
}
|
||||
|
||||
; Update coordinate display based on mouse position
|
||||
UpdateCoordinates() {
|
||||
global hwnd, coordText
|
||||
|
||||
if (!hwnd) {
|
||||
hwnd := FindDesignerWindow()
|
||||
if (!hwnd) {
|
||||
coordText.Value := "Window not found"
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
; Get mouse position
|
||||
CoordMode("Mouse", "Screen")
|
||||
MouseGetPos(&mouseX, &mouseY)
|
||||
|
||||
; Get client area
|
||||
client := GetClientArea(hwnd)
|
||||
if (!client) {
|
||||
coordText.Value := "Could not get client area"
|
||||
return
|
||||
}
|
||||
|
||||
; Calculate phone coordinates
|
||||
phoneX := mouseX - client.x
|
||||
phoneY := mouseY - client.y
|
||||
|
||||
; Check if within phone bounds
|
||||
if (phoneX >= 0 && phoneX < PHONE_WIDTH && phoneY >= 0 && phoneY < PHONE_HEIGHT) {
|
||||
coordText.Value := "X: " . phoneX . " Y: " . phoneY
|
||||
} else {
|
||||
coordText.Value := "Outside phone area"
|
||||
}
|
||||
}
|
||||
|
||||
; Timer to update coordinates
|
||||
SetTimer(UpdateCoordinates, 100)
|
||||
|
||||
; Main
|
||||
LogMessage("Starting interactive test helper")
|
||||
|
||||
; Start designer if not running
|
||||
if (!FindDesignerWindow()) {
|
||||
LogMessage("Starting designer...")
|
||||
StartDesigner()
|
||||
Sleep(3000)
|
||||
}
|
||||
|
||||
CreateStatusGui()
|
||||
RefreshStatus()
|
||||
|
||||
; Keep running
|
||||
return
|
||||
77
designer/test/test_manual_clicks.ahk
Normal file
77
designer/test/test_manual_clicks.ahk
Normal file
@@ -0,0 +1,77 @@
|
||||
; Mosis Designer Manual Click Test
|
||||
; AutoHotkey v2
|
||||
; Tests clicking on specific app icons with an already-running designer
|
||||
|
||||
#Requires AutoHotkey v2.0
|
||||
#Include "lib\utils.ahk"
|
||||
|
||||
; Wait for designer to be running
|
||||
LogMessage("Looking for designer window...")
|
||||
LogMessage("Please start the designer manually if not running:")
|
||||
LogMessage(" cd designer && build\\Debug\\mosis-designer.exe ..\\src\\main\\assets\\apps\\home\\home.rml")
|
||||
|
||||
hwnd := WaitForDesignerWindow(30000) ; 30 second timeout
|
||||
|
||||
if (!hwnd) {
|
||||
MsgBox("Designer window not found after 30 seconds. Exiting.")
|
||||
ExitApp()
|
||||
}
|
||||
|
||||
LogMessage("Found designer window: " . hwnd)
|
||||
|
||||
; Get and display client area info
|
||||
client := GetClientArea(hwnd)
|
||||
if (client) {
|
||||
LogMessage("Client area: x=" . client.x . " y=" . client.y . " w=" . client.w . " h=" . client.h)
|
||||
}
|
||||
|
||||
; Interactive test loop
|
||||
MsgBox("Designer found! Click OK to start click tests.`n`nWe will click on the Phone app icon.")
|
||||
|
||||
; Test 1: Click Phone app
|
||||
LogMessage("Test 1: Clicking Phone app at (67, 120)")
|
||||
ClickPhone(67, 120, hwnd)
|
||||
Sleep(500)
|
||||
|
||||
result := MsgBox("Did the screen change to the Dialer?", "Verify", "YesNo")
|
||||
if (result = "Yes") {
|
||||
LogMessage("Test 1 PASSED: Phone app click worked")
|
||||
} else {
|
||||
LogMessage("Test 1 FAILED: Phone app click did not work")
|
||||
}
|
||||
|
||||
; Give user time to see result
|
||||
Sleep(1000)
|
||||
|
||||
; Test 2: Click Messages dock
|
||||
MsgBox("Now testing Messages dock item. Click OK to continue.")
|
||||
LogMessage("Test 2: Clicking Messages dock at (202, 920)")
|
||||
ClickPhone(202, 920, hwnd)
|
||||
Sleep(500)
|
||||
|
||||
result := MsgBox("Did the screen change to Messages?", "Verify", "YesNo")
|
||||
if (result = "Yes") {
|
||||
LogMessage("Test 2 PASSED: Messages dock click worked")
|
||||
} else {
|
||||
LogMessage("Test 2 FAILED: Messages dock click did not work")
|
||||
}
|
||||
|
||||
Sleep(1000)
|
||||
|
||||
; Test 3: Different coordinates for Messages (in case dock is higher)
|
||||
MsgBox("Testing alternative dock position. Click OK to continue.")
|
||||
LogMessage("Test 3: Clicking Messages at alternative position (202, 880)")
|
||||
ClickPhone(202, 880, hwnd)
|
||||
Sleep(500)
|
||||
|
||||
result := MsgBox("Did anything happen?", "Verify", "YesNo")
|
||||
if (result = "Yes") {
|
||||
LogMessage("Test 3: Alternative position worked better")
|
||||
} else {
|
||||
LogMessage("Test 3: Alternative position also failed")
|
||||
}
|
||||
|
||||
; Summary
|
||||
MsgBox("Tests complete! Check test_run.log for results.`n`nPath: " . A_ScriptDir . "\test_run.log")
|
||||
|
||||
ExitApp()
|
||||
194
designer/test/test_navigation.ahk
Normal file
194
designer/test/test_navigation.ahk
Normal file
@@ -0,0 +1,194 @@
|
||||
; Mosis Designer Navigation Test
|
||||
; AutoHotkey v2
|
||||
; Tests: Click on app icons and verify navigation occurs
|
||||
|
||||
#Requires AutoHotkey v2.0
|
||||
#Include "lib\utils.ahk"
|
||||
|
||||
; Test configuration
|
||||
testsPassed := 0
|
||||
testsFailed := 0
|
||||
|
||||
; Main test function
|
||||
RunNavigationTests() {
|
||||
global testsPassed, testsFailed
|
||||
|
||||
LogMessage("========================================")
|
||||
LogMessage("Starting Navigation Tests")
|
||||
LogMessage("========================================")
|
||||
|
||||
; Clean up any previous runs
|
||||
KillDesigner()
|
||||
ClearLogs()
|
||||
|
||||
; Start the designer
|
||||
pid := StartDesigner()
|
||||
if (!pid) {
|
||||
LogMessage("FATAL: Could not start designer")
|
||||
return false
|
||||
}
|
||||
|
||||
; Wait for window to appear
|
||||
LogMessage("Waiting for designer window...")
|
||||
hwnd := WaitForDesignerWindow()
|
||||
if (!hwnd) {
|
||||
LogMessage("FATAL: Designer window did not appear")
|
||||
KillDesigner()
|
||||
return false
|
||||
}
|
||||
LogMessage("Designer window found: " . hwnd)
|
||||
|
||||
; Wait for document to load
|
||||
Sleep(2000) ; Give time for initial document load
|
||||
|
||||
; Check log for successful load
|
||||
if (!WaitForLog("Document loaded successfully", 5000)) {
|
||||
LogMessage("WARNING: Did not detect document load confirmation")
|
||||
}
|
||||
|
||||
; Run individual tests
|
||||
TestClickDialer(hwnd)
|
||||
TestClickMessages(hwnd)
|
||||
TestClickContacts(hwnd)
|
||||
TestClickSettings(hwnd)
|
||||
|
||||
; Clean up
|
||||
LogMessage("Closing designer...")
|
||||
CloseDesigner(hwnd)
|
||||
Sleep(500)
|
||||
|
||||
; Report results
|
||||
LogMessage("========================================")
|
||||
LogMessage("Navigation Tests Complete")
|
||||
LogMessage("Passed: " . testsPassed)
|
||||
LogMessage("Failed: " . testsFailed)
|
||||
LogMessage("========================================")
|
||||
|
||||
return (testsFailed = 0)
|
||||
}
|
||||
|
||||
; Test: Click on Phone/Dialer app
|
||||
TestClickDialer(hwnd) {
|
||||
global testsPassed, testsFailed
|
||||
|
||||
LogMessage("--- Test: Click Dialer ---")
|
||||
|
||||
; First go home to ensure clean state
|
||||
; (In future, could send Home key)
|
||||
Sleep(500)
|
||||
|
||||
; Click the phone app
|
||||
if (!ClickApp("phone", hwnd)) {
|
||||
testsFailed++
|
||||
return
|
||||
}
|
||||
|
||||
Sleep(1000)
|
||||
|
||||
; Check log for navigation
|
||||
; Note: The log might say "dialer" because that's the screen name
|
||||
result := CheckLogFor("Navigated to: dialer")
|
||||
if (result) {
|
||||
LogMessage("Navigation confirmed: " . result)
|
||||
testsPassed++
|
||||
} else {
|
||||
; Also check if screen was loaded
|
||||
result := CheckLogFor("Loaded screen: apps/dialer")
|
||||
if (result) {
|
||||
LogMessage("Screen load confirmed: " . result)
|
||||
testsPassed++
|
||||
} else {
|
||||
LogMessage("Navigation not detected in log")
|
||||
testsFailed++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
; Test: Click on Messages app
|
||||
TestClickMessages(hwnd) {
|
||||
global testsPassed, testsFailed
|
||||
|
||||
LogMessage("--- Test: Click Messages ---")
|
||||
|
||||
; We're likely on dialer now, need to go back first
|
||||
; For now, just click messages dock item
|
||||
Sleep(500)
|
||||
|
||||
if (!ClickDock("messages", hwnd)) {
|
||||
testsFailed++
|
||||
return
|
||||
}
|
||||
|
||||
Sleep(1000)
|
||||
|
||||
result := CheckLogFor("Loaded screen: apps/messages")
|
||||
if (result) {
|
||||
LogMessage("Screen load confirmed: " . result)
|
||||
testsPassed++
|
||||
} else {
|
||||
LogMessage("Navigation not detected in log")
|
||||
testsFailed++
|
||||
}
|
||||
}
|
||||
|
||||
; Test: Click on Contacts app
|
||||
TestClickContacts(hwnd) {
|
||||
global testsPassed, testsFailed
|
||||
|
||||
LogMessage("--- Test: Click Contacts ---")
|
||||
Sleep(500)
|
||||
|
||||
if (!ClickDock("contacts", hwnd)) {
|
||||
testsFailed++
|
||||
return
|
||||
}
|
||||
|
||||
Sleep(1000)
|
||||
|
||||
result := CheckLogFor("Loaded screen: apps/contacts")
|
||||
if (result) {
|
||||
LogMessage("Screen load confirmed: " . result)
|
||||
testsPassed++
|
||||
} else {
|
||||
LogMessage("Navigation not detected in log")
|
||||
testsFailed++
|
||||
}
|
||||
}
|
||||
|
||||
; Test: Click on Settings app
|
||||
TestClickSettings(hwnd) {
|
||||
global testsPassed, testsFailed
|
||||
|
||||
LogMessage("--- Test: Click Settings ---")
|
||||
Sleep(500)
|
||||
|
||||
; Settings is on the main grid, not dock
|
||||
; But we're on contacts now, so we need to navigate
|
||||
; For this test, we'll just verify the click mechanics work
|
||||
|
||||
if (!ClickApp("settings", hwnd)) {
|
||||
; This will likely fail since we're not on home screen
|
||||
; But let's see what happens
|
||||
LogMessage("Click attempted (may fail if not on home)")
|
||||
testsFailed++
|
||||
return
|
||||
}
|
||||
|
||||
Sleep(1000)
|
||||
|
||||
result := CheckLogFor("Loaded screen: apps/settings")
|
||||
if (result) {
|
||||
LogMessage("Screen load confirmed: " . result)
|
||||
testsPassed++
|
||||
} else {
|
||||
LogMessage("Navigation not detected (expected if not on home screen)")
|
||||
testsFailed++
|
||||
}
|
||||
}
|
||||
|
||||
; Run the tests
|
||||
RunNavigationTests()
|
||||
|
||||
; Keep script alive briefly for logging
|
||||
Sleep(1000)
|
||||
ExitApp()
|
||||
124
designer/test/visual_click_test.ahk
Normal file
124
designer/test/visual_click_test.ahk
Normal file
@@ -0,0 +1,124 @@
|
||||
; Visual click test - verifies clicks by window interaction
|
||||
#Requires AutoHotkey v2.0
|
||||
|
||||
; Configuration
|
||||
DESIGNER_EXE := A_ScriptDir . "\..\build\Debug\mosis-designer.exe"
|
||||
HOME_RML := A_ScriptDir . "\..\..\src\main\assets\apps\home\home.rml"
|
||||
WINDOW_TITLE := "Mosis Designer"
|
||||
|
||||
resultFile := A_ScriptDir . "\visual_test_result.txt"
|
||||
if (FileExist(resultFile))
|
||||
FileDelete(resultFile)
|
||||
|
||||
Log(msg) {
|
||||
global resultFile
|
||||
FileAppend(msg . "`n", resultFile, "UTF-8")
|
||||
}
|
||||
|
||||
Log("=== Visual Click Test ===")
|
||||
Log("Time: " . FormatTime(, "yyyy-MM-dd HH:mm:ss"))
|
||||
|
||||
; Check if designer is running, if not start it
|
||||
hwnd := WinExist(WINDOW_TITLE)
|
||||
if (!hwnd) {
|
||||
Log("Starting designer...")
|
||||
Run(DESIGNER_EXE . ' "' . HOME_RML . '"', A_ScriptDir)
|
||||
|
||||
; Wait for window
|
||||
startTime := A_TickCount
|
||||
while (A_TickCount - startTime < 15000) {
|
||||
hwnd := WinExist(WINDOW_TITLE)
|
||||
if (hwnd)
|
||||
break
|
||||
Sleep(200)
|
||||
}
|
||||
|
||||
if (!hwnd) {
|
||||
Log("ERROR: Window did not appear")
|
||||
ExitApp(1)
|
||||
}
|
||||
Log("Designer started")
|
||||
Sleep(3000) ; Wait for full initialization
|
||||
} else {
|
||||
Log("Using existing designer window")
|
||||
}
|
||||
|
||||
Log("Window HWND: " . hwnd)
|
||||
|
||||
; Get window info
|
||||
WinGetPos(&winX, &winY, &winW, &winH, hwnd)
|
||||
Log("Window: " . winW . "x" . winH . " at (" . winX . "," . winY . ")")
|
||||
|
||||
; Calculate scale and client area
|
||||
scale := (winW / 556 + winH / 1000) / 2
|
||||
titleBar := Round(31 * scale)
|
||||
border := Round(8 * scale)
|
||||
clientX := winX + border
|
||||
clientY := winY + titleBar
|
||||
Log("Scale: " . scale . ", Client offset: (" . border . "," . titleBar . ")")
|
||||
|
||||
; Activate window
|
||||
WinActivate(hwnd)
|
||||
WinWaitActive(hwnd, , 3)
|
||||
Sleep(500)
|
||||
|
||||
Log("")
|
||||
Log("=== Click Sequence ===")
|
||||
|
||||
; Function to click
|
||||
DoClick(phoneX, phoneY, name) {
|
||||
global clientX, clientY, scale, hwnd
|
||||
|
||||
screenX := Round(clientX + phoneX * scale)
|
||||
screenY := Round(clientY + phoneY * scale)
|
||||
|
||||
Log("Clicking " . name . " at (" . phoneX . "," . phoneY . ") -> screen(" . screenX . "," . screenY . ")")
|
||||
|
||||
; Make sure window is active
|
||||
WinActivate(hwnd)
|
||||
Sleep(100)
|
||||
|
||||
; Move mouse first, then click
|
||||
MouseMove(screenX, screenY)
|
||||
Sleep(50)
|
||||
Click()
|
||||
Sleep(100)
|
||||
|
||||
Log("Click sent")
|
||||
}
|
||||
|
||||
; Test sequence:
|
||||
; 1. Click on Phone app (should navigate to dialer)
|
||||
Log("")
|
||||
Log("Step 1: Click Phone app icon")
|
||||
DoClick(67, 120, "Phone")
|
||||
Sleep(2000)
|
||||
|
||||
; 2. Click somewhere to go back (simulate back button or click home dock)
|
||||
Log("")
|
||||
Log("Step 2: Wait and observe...")
|
||||
Sleep(1000)
|
||||
|
||||
; 3. Click on Messages dock
|
||||
Log("")
|
||||
Log("Step 3: Click Messages dock")
|
||||
DoClick(202, 920, "Messages Dock")
|
||||
Sleep(2000)
|
||||
|
||||
; 4. Click on Contacts dock
|
||||
Log("")
|
||||
Log("Step 4: Click Contacts dock")
|
||||
DoClick(337, 920, "Contacts Dock")
|
||||
Sleep(2000)
|
||||
|
||||
Log("")
|
||||
Log("=== Test Sequence Complete ===")
|
||||
Log("Please verify visually that navigation occurred:")
|
||||
Log("- After Phone click: Should show Dialer screen")
|
||||
Log("- After Messages click: Should show Messages screen")
|
||||
Log("- After Contacts click: Should show Contacts screen")
|
||||
Log("")
|
||||
Log("Leaving designer running for manual inspection.")
|
||||
Log("Close it manually when done.")
|
||||
|
||||
ExitApp(0)
|
||||
12
designer/vcpkg.json
Normal file
12
designer/vcpkg.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "mosis-designer",
|
||||
"version-string": "0.1.0",
|
||||
"description": "Desktop designer and testing tool for Mosis virtual smartphone UI",
|
||||
"dependencies": [
|
||||
"glfw3",
|
||||
"freetype",
|
||||
"lua",
|
||||
"libpng",
|
||||
"nlohmann-json"
|
||||
]
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user