# UIDefEdit A native Android C++ application for visually designing UI layouts conforming to the [ui-layout-def](docs/0-proposal-of-impl.md) XML specification (v1.6). Built entirely on-device in [Termux](https://termux.dev/) — no desktop host, no Android Studio. ## What It Does UIDefEdit provides a visual canvas for building UI layout definitions: - **Draw wireframes** — place controls from a categorized toolbox onto a pan/zoom canvas - **Define windows** — create windows from wireframe templates with slot overrides, or build standalone layouts - **Edit properties** — type-aware attribute editor (text, int, bool, enum, color) with validation - **Manage documents** — create new documents targeting different control sets (WinForms, Avalonia, Web Awesome) - **Save/Load XML** — full round-trip fidelity preserving unedited sections (views, validation-rules, state-management) - **XSD validation** — validate documents against 8 bundled XSD schemas with error/warning panel - **File browser** — custom ImGui file picker for `/sdcard/` with open, save-as, and auto-save - **Undo/redo** — full command-pattern undo stack for all canvas operations + three-finger tap gesture Controls render as color-coded rounded rectangles (blue=input, green=display, gray=container, orange=list) with nesting, resize handles, and grid snap. ## Screenshots *Portrait layout: toolbar at top, canvas center, document tree bottom. Toolbox slides in from left, properties from right.* ## Implementation Status All 8 milestones are complete. | Milestone | Status | |-----------|--------| | 1. Project skeleton (NativeActivity + EGL + ImGui + canvas pan/zoom) | Done | | 2. Control registry + toolbox (XML parsing, inheritance resolution) | Done | | 3. Canvas drawing (place, move, resize, nesting, selection, undo/redo) | Done | | 4. Property editor (type-specific widgets, validation highlights) | Done | | 5. Wireframes & windows (slot marking, overrides, document tree) | Done | | 6. XML I/O (save/load with round-trip, file browser, auto-save) | Done | | 7. XSD validation (libxml2 via dlopen, error panel) | Done | | 8. Polish & APK packaging (gestures, culling, responsive scaling) | Done | ## Architecture ``` ANativeActivity_onCreate (main.cpp) │ Direct callbacks on main UI thread │ timerfd + ALooper for ~60fps rendering │ EGL/GLES3 context │ Multi-touch: pinch zoom, double-tap zoom, three-finger undo │ ├── App (app.cpp — lifecycle, docking layout, frame orchestration) │ ├── UI layer (Dear ImGui, docking branch) │ ├── toolbar — top bar: New/Open/Save/Validate + undo/redo │ ├── toolbox — left slide-in: control palette by category │ ├── properties — right slide-in: attribute editor │ ├── canvas — central: pan/zoom drawing surface with viewport culling │ ├── doc_tree — bottom collapsible: wireframe/window hierarchy │ ├── validation_panel — bottom tab: XSD error/warning list │ └── file_dialog — custom /sdcard/ browser (open/save-as) │ ├── Model layer │ ├── Document — root: version, platform, pattern_includes, wireframes, windows │ ├── ControlRegistry — parsed control definitions with resolved inheritance │ ├── ControlInstance — placed control: id, position, properties, parent/children tree │ ├── WireframeDef — template layout with named SlotDefs │ └── WindowDef — wireframe-based (slot overrides) or standalone (template) │ ├── XML engine │ ├── xml_reader/writer — pugixml DOM (round-trips non-visual sections verbatim) │ └── xsd_validator — libxml2 via dlopen (graceful degradation if unavailable) │ └── Utilities ├── undo_redo — command pattern stack (100 action cap, drag grouping) └── platform — AAssetManager helpers, path resolution ``` ### Key Design Decisions **No `android_native_app_glue`.** The standard NDK app_glue creates a background pthread for the app's main loop and communicates via pipes. This threading model fails when built with Termux's native Clang toolchain. UIDefEdit uses direct `ANativeActivity` callbacks on the main UI thread with a `timerfd` registered on the main `ALooper` for frame rendering. This is single-threaded and reliable. **Java wrapper required.** Android 16 (API 36) requires `android:hasCode="true"` with a valid `classes.dex`. A minimal `DebugActivity extends NativeActivity` Java class serves as the entry point and provides soft keyboard support (`showSoftInput`/`pollUnicodeChar`). **RPATH stripping.** Termux's Clang embeds `RPATH=/data/data/com.termux/files/usr/lib` into shared libraries. This path is inaccessible from an installed APK's process. The build script strips it with `patchelf --remove-rpath`. **GLES3 on the main thread.** EGL context and all GL calls happen on the main UI thread (same thread as Activity lifecycle callbacks). This avoids the cross-thread context issues that plagued the app_glue approach. **libxml2 via dlopen.** XSD validation uses runtime `dlopen("libxml2.so")` instead of link-time dependency. This avoids bundling Termux's libxml2 and its transitive dependencies (ICU, iconv, lzma) which conflict with Android's system libraries. Validation gracefully degrades if libxml2 is not available. **v1.6 XML namespace compliance.** Documents use `xmlns="http://quadarax.com/ui-layout-definition"` and follow the XSD document order: `validation-rules` → `patterns` (with wireframes inside) → `views` → `window-includes` → `window`. ## Project Structure ``` ├── src/ # C++ sources (~4,500 lines) │ ├── main.cpp # ANativeActivity_onCreate, EGL, ImGui loop, input │ ├── app.h / app.cpp # App state machine, docking layout │ ├── canvas/ │ │ ├── canvas.h / canvas.cpp # Pan/zoom surface, control rendering, viewport culling │ │ ├── grid.h / grid.cpp # Snap grid │ │ └── selection.h / selection.cpp # Hit testing, multi-select, resize handles │ ├── model/ │ │ ├── control_registry.h/.cpp # Parse control XMLs, resolve inheritance │ │ ├── control_instance.h/.cpp # Placed control with position + properties │ │ ├── document.h # Document root (PatternInclude, WireframeDef, WindowDef) │ │ ├── wireframe_def.h # Wireframe with slots │ │ └── window_def.h # Window (template or slot-overrides) │ ├── xml/ │ │ ├── xml_reader.h / xml_reader.cpp # Load XML → Document model │ │ ├── xml_writer.h / xml_writer.cpp # Document model → XML file │ │ └── xsd_validator.h / xsd_validator.cpp # XSD validation (dlopen libxml2) │ ├── ui/ │ │ ├── toolbar.h / toolbar.cpp # Top menu bar + dialogs (New Doc, New Wireframe, New Window) │ │ ├── toolbox.h / toolbox.cpp # Left slide-in: control palette │ │ ├── properties.h / properties.cpp # Right slide-in: attribute editor │ │ ├── doc_tree.h / doc_tree.cpp # Bottom panel: wireframe/window hierarchy │ │ ├── file_dialog.h / file_dialog.cpp # Custom file browser │ │ └── validation_panel.h / validation_panel.cpp # XSD error/warning list │ └── util/ │ ├── undo_redo.h / undo_redo.cpp # Undo stack │ └── platform.h / platform.cpp # AAssetManager helpers ├── android/ │ ├── AndroidManifest.xml # hasCode=true, DebugActivity, portrait, GLES3 │ ├── build-apk.sh # Full APK pipeline: compile, dex, aapt2, zipalign, sign │ └── java/com/uidef/edit/ │ └── DebugActivity.java # NativeActivity subclass with keyboard support ├── assets/ │ ├── controls/ # 4 control set XMLs (abstract, winform, avalonia, web) │ ├── schemas/ # 8 XSD schema files (ui-layout-def v1.6) │ └── fonts/ # Roboto-Regular.ttf ├── third_party/ │ ├── imgui/ # Dear ImGui (docking branch) │ └── pugixml/ # pugixml 1.14+ (2 files) ├── test/ # Sample XML documents for testing │ ├── test-basic.xml # Abstract controls, 1 wireframe, 1 window │ ├── test-avalonia.xml # Avalonia controls, 2 wireframes, 2 windows │ ├── test-webawesome.xml # Web Awesome controls, form layout │ ├── test-complex.xml # WinForms, 3 wireframes, 3 windows, 50+ controls │ └── test-invalid.xml # Intentionally invalid for validation testing ├── docs/ │ ├── 0-proposal-of-impl.md # Full architecture specification │ └── 1-implementation-todo.md # Milestone-ordered task checklist ├── CMakeLists.txt # C++17, shared library, links EGL/GLESv3/android/log └── build/ # Build output ├── libuidef-tool.so # ~8 MB (debug, arm64-v8a) └── uidef-edit.apk # ~4.6 MB (signed, debug) ``` ## Libraries | Library | Version | Purpose | |---------|---------|---------| | [Dear ImGui](https://github.com/ocornut/imgui) (docking branch) | 1.91+ | All UI rendering — immediate mode | | [pugixml](https://github.com/zeux/pugixml) | 1.14+ | XML DOM read/write for documents and control definitions | | libxml2 | 2.12+ | XSD schema validation (loaded via dlopen at runtime) | ## Building ### Prerequisites Termux on an ARM64 Android device: ```bash pkg install clang cmake make git patchelf pkg install ndk-sysroot android-ndk ``` Additional tools for APK packaging: ```bash pkg install aapt2 apksigner zipalign d8 javac ``` An `android.jar` (API 28+) is needed at `$HOME/android.jar` for resource linking. ### Build ```bash # Configure (only needed once) cmake -B build -DCMAKE_BUILD_TYPE=Debug # Compile cmake --build build -j$(nproc) # Package APK bash android/build-apk.sh ``` The CMake build uses Termux's native Clang (which already targets `aarch64-unknown-linux-android24`) and links against `/system/lib64/` for EGL and GLES3. No separate NDK toolchain file is needed. ### Install ```bash cp build/uidef-edit.apk ~/storage/downloads/ termux-open ~/storage/downloads/uidef-edit.apk ``` Direct `pm install` from Termux is blocked by SELinux on most devices. Use `termux-open` to launch the system package installer. ## Touch Gestures | Gesture | Action | |---------|--------| | Single tap | Select control / place control (in placement mode) | | Long press (500ms) | Context menu (Delete, Duplicate, Mark Slot, etc.) | | Single-finger drag | Pan canvas (empty area) / move control (selected) | | Two-finger pinch | Zoom in/out (0.25x – 4.0x) | | Double tap | Toggle between 100% zoom and zoom-to-fit | | Three-finger tap | Undo | | Scroll wheel | Zoom (external mouse) | | Middle-click drag | Pan (external mouse) | ## Control Sets Four control definition files are bundled as APK assets: | File | Controls | Platform | |------|----------|----------| | `abstract-controls.xml` | Label, TextBox, Button, Panel, SplitPanel, TabControl, ListBox, DataGrid, etc. | `basic` (always loaded) | | `winform-controls.xml` | WinForms-specific controls inheriting from abstract | `winforms` | | `avalonia-controls.xml` | Avalonia-specific controls inheriting from abstract | `avalonia` | | `webawesome-controls.xml` | wa-button, wa-input, wa-card, wa-dialog, etc. | `web` | The control registry resolves `inherits` chains (topological sort) so each control has a complete standalone attribute list. ## XML Round-Trip UIDefEdit preserves sections it doesn't edit (views, validation-rules, state-management, etc.) as raw pugixml DOM nodes, merging them back on save in the correct XSD document order. The pattern-include elements carry `src` and `platform` attributes matching the `PatternIncludeType` in the v1.6 schema. ## Implementation Notes ### Termux-Specific Challenges Building a NativeActivity APK entirely within Termux presented several challenges: 1. **RPATH poisoning.** Termux's Clang embeds its library path as RPATH/RUNPATH in all shared libraries. Android's bionic linker rejects these paths when running outside Termux. Solution: `patchelf --remove-rpath` in the APK build script. 2. **`libc++_shared.so` bundling.** The NDK's libc++ is not available on the device's system libraries. It must be bundled in the APK at `lib/arm64-v8a/libc++_shared.so`. 3. **`android_native_app_glue` incompatibility.** The standard NDK helper creates a background pthread for the app loop. This threading model causes ANR (Application Not Responding) when the .so is built with Termux's native Clang. The fix is bypassing app_glue entirely and using direct `ANativeActivity` callbacks with `timerfd` + `ALooper` for frame rendering. 4. **`hasCode="false"` crashes on Android 16.** Modern Android requires `hasCode="true"` with a valid `classes.dex` in the APK, even for NativeActivity apps. A minimal Java wrapper class is needed. 5. **No logcat access.** Termux cannot read logcat output from other apps (different UID). Debugging is done via file-based logging to the app's internal data path. 6. **No `pm install` from Termux.** SELinux blocks direct package installation from Termux's shell. Use `termux-open` to launch the system installer. 7. **libxml2 transitive dependencies.** Bundling Termux's libxml2.so brings in libicuuc, libicudata, libiconv, and liblzma — all with versioned sonames that conflict with Android's system libraries. Solution: load libxml2 via `dlopen` at runtime instead of linking at build time. ### Rendering Architecture The rendering loop is driven by a `timerfd` at ~60fps, registered with the main thread's `ALooper`: ``` Main thread ALooper dispatch cycle: ├── timerfd fires → timer_callback → MainLoopStep() │ ├── ImGui_ImplOpenGL3_NewFrame() │ ├── ImGui_ImplAndroid_NewFrame() │ ├── ImGui::NewFrame() │ ├── App::update() (all UI rendering) │ ├── ImGui::Render() │ ├── glClear + ImGui_ImplOpenGL3_RenderDrawData() │ └── eglSwapBuffers() └── input queue fires → input_looper_callback → ProcessInput() └── AInputQueue_getEvent → handleInputEvent → ImGui_ImplAndroid_HandleInputEvent ``` Input events are attached to the same ALooper with a separate callback, providing low-latency touch handling. ### Performance - **Viewport culling**: Controls fully outside the visible canvas area skip all draw calls - **Responsive scaling**: UI scale factor derived from screen width (`width / 540dp`) - **Target**: 60 FPS on mid-range devices with 100+ controls ### Memory Model - `std::unique_ptr` for parent-to-child control ownership - Raw pointers for child-to-parent back-references - `Document` owns all wireframes and windows - Undo actions store deltas, not snapshots (capped at 100 actions) ## License *To be determined.*