work in progress

This commit is contained in:
2026-01-16 12:43:06 +01:00
parent 77a9579025
commit c8ee7defe8
236 changed files with 11405 additions and 28 deletions

5
.gitattributes vendored
View File

@@ -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
View File

@@ -5,4 +5,4 @@
build
.cxx
.DS_Store
/designer/test/*test_result.txt

187
CLAUDE.md
View File

@@ -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
View 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)

View 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
)

View 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

View 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

View 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

View 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
View 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;
}

View 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

View 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

View 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, &currentRect);
// 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

View 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
View File

@@ -0,0 +1,7 @@
{
"name": "designer-test",
"version-string": "0.1.0",
"dependencies": [
"nlohmann-json"
]
}

104
designer/CMakeLists.txt Normal file
View 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
)

View 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_ */

View 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
View 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);
}

View 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;
}

View 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);

View 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

View 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

View 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
View 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

View 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

View 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
View 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
View 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);
}

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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
View 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\)

View 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
View 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
View 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()

View 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
View 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
}
}

View 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()

View 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()

View 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

View 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()

View 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()

View 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
View 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.

Some files were not shown because too many files have changed in this diff Show More