From 339e6f9806162f2eb54e15e6f20bfcdb487ff802 Mon Sep 17 00:00:00 2001 From: antono2 Date: Sat, 3 Oct 2026 05:27:08 +0200 Subject: [PATCH 01/29] Package portable releases and integrate accessible gallery and touch controls --- .github/actions/setup-v/action.yml | 10 +- .github/workflows/additional-examples.yml | 2 +- .github/workflows/compile.yml | 6 +- .github/workflows/release-packages.yml | 128 +++++++++++ .gitignore | 2 + IMGUI_REVISION | 2 +- README.md | 19 +- docs/installing-releases.md | 84 +++++++ docs/releasing.md | 35 +++ docs/testing.md | 15 ++ examples/android_touch/AndroidManifest.xml | 3 +- examples/android_touch/README.md | 17 ++ .../accessibility_d_release_accessibility.v | 94 ++++++++ examples/android_touch/accessibility_state.h | 7 + examples/android_touch/ui.v | 3 + examples/glfw_vulkan/main.c.v | 3 +- examples/widget_gallery/README.md | 14 ++ .../widget_gallery/accessibility_bridge.h | 32 +++ .../accessibility_d_release_accessibility.v | 111 ++++++++++ examples/widget_gallery/main.v | 130 ++++++----- packaging/MICROSOFT-RUNTIME-NOTICE.txt | 7 + packaging/README-linux.txt | 19 ++ packaging/README-windows.txt | 15 ++ packaging/ROBOTO-LICENSE.txt | 201 +++++++++++++++++ packaging/VULKAN-LOADER-LICENSE.txt | 207 ++++++++++++++++++ scripts/build_android.sh | 2 +- scripts/build_release_linux.sh | 25 +++ scripts/build_release_windows.ps1 | 52 +++++ scripts/collect_licenses.py | 43 ++++ scripts/package_linux.py | 115 ++++++++++ scripts/package_windows.ps1 | 56 +++++ scripts/sign_android_release.py | 70 ++++++ scripts/test_android_accessibility.sh | 32 +++ .../accessibility/AccessibilitySmoke.java | 150 +++++++++++++ .../android/accessibility/AndroidManifest.xml | 6 + tests/desktop/accessibility.py | 52 +++++ 36 files changed, 1702 insertions(+), 67 deletions(-) create mode 100644 .github/workflows/release-packages.yml create mode 100644 docs/installing-releases.md create mode 100644 docs/releasing.md create mode 100644 examples/android_touch/accessibility_d_release_accessibility.v create mode 100644 examples/android_touch/accessibility_state.h create mode 100644 examples/widget_gallery/accessibility_bridge.h create mode 100644 examples/widget_gallery/accessibility_d_release_accessibility.v create mode 100644 packaging/MICROSOFT-RUNTIME-NOTICE.txt create mode 100644 packaging/README-linux.txt create mode 100644 packaging/README-windows.txt create mode 100644 packaging/ROBOTO-LICENSE.txt create mode 100644 packaging/VULKAN-LOADER-LICENSE.txt create mode 100755 scripts/build_release_linux.sh create mode 100644 scripts/build_release_windows.ps1 create mode 100644 scripts/collect_licenses.py create mode 100755 scripts/package_linux.py create mode 100644 scripts/package_windows.ps1 create mode 100644 scripts/sign_android_release.py create mode 100755 scripts/test_android_accessibility.sh create mode 100644 tests/android/accessibility/AccessibilitySmoke.java create mode 100644 tests/android/accessibility/AndroidManifest.xml create mode 100644 tests/desktop/accessibility.py diff --git a/.github/actions/setup-v/action.yml b/.github/actions/setup-v/action.yml index dbfecdb..cd429de 100644 --- a/.github/actions/setup-v/action.yml +++ b/.github/actions/setup-v/action.yml @@ -18,7 +18,15 @@ runs: repository: vlang/tccbin ref: d6e7ac1b1bcc98aed734a6ecbfa8509f24606c74 path: toolchain/v/thirdparty/tcc - - shell: bash + - if: runner.os != 'Windows' + shell: bash run: | make -C toolchain/v local=1 echo "$GITHUB_WORKSPACE/toolchain/v" >> "$GITHUB_PATH" + - if: runner.os == 'Windows' + shell: cmd + working-directory: toolchain/v + run: makev.bat -msvc --local + - if: runner.os == 'Windows' + shell: pwsh + run: '"$env:GITHUB_WORKSPACE\toolchain\v" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append' diff --git a/.github/workflows/additional-examples.yml b/.github/workflows/additional-examples.yml index df35ca7..9b50e7f 100644 --- a/.github/workflows/additional-examples.yml +++ b/.github/workflows/additional-examples.yml @@ -26,7 +26,7 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: repository: antono2/imgui - ref: 658d1fa0e863c3d0345ffdd9685fee05876a7648 + ref: 253cb56d86ab6bb79283980d57ea42e64f091734 path: build/modules/antono2/imgui submodules: recursive - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 diff --git a/.github/workflows/compile.yml b/.github/workflows/compile.yml index dd5d7a7..ffee0cf 100644 --- a/.github/workflows/compile.yml +++ b/.github/workflows/compile.yml @@ -26,7 +26,7 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: repository: antono2/imgui - ref: a28bfc544a46fd80221bdfe0aa45e161df9782d0 + ref: c093593eeb5685ab4c40b308b9773b51fec37ffe path: source/modules/antono2/imgui submodules: recursive - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 @@ -89,7 +89,7 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: repository: antono2/imgui - ref: a28bfc544a46fd80221bdfe0aa45e161df9782d0 + ref: c093593eeb5685ab4c40b308b9773b51fec37ffe path: source/modules/antono2/imgui submodules: recursive - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 @@ -201,7 +201,7 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: repository: antono2/imgui - ref: a28bfc544a46fd80221bdfe0aa45e161df9782d0 + ref: c093593eeb5685ab4c40b308b9773b51fec37ffe path: source/modules/antono2/imgui submodules: recursive - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 diff --git a/.github/workflows/release-packages.yml b/.github/workflows/release-packages.yml new file mode 100644 index 0000000..de1a3f4 --- /dev/null +++ b/.github/workflows/release-packages.yml @@ -0,0 +1,128 @@ +name: Portable release packages +on: + pull_request: + branches: [master] + workflow_dispatch: +permissions: + contents: read +jobs: + desktop: + strategy: + fail-fast: false + matrix: + os: [ubuntu-24.04, windows-2022] + variant: [docking, standard] + include: + - variant: docking + imgui-ref: c093593eeb5685ab4c40b308b9773b51fec37ffe + - variant: standard + imgui-ref: 253cb56d86ab6bb79283980d57ea42e64f091734 + runs-on: ${{ matrix.os }} + timeout-minutes: 45 + env: + VMODULES: ${{ github.workspace }}/build/modules + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + - uses: ./.github/actions/setup-v + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + repository: antono2/imgui + ref: ${{ matrix.imgui-ref }} + path: build/modules/antono2/imgui + submodules: recursive + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + repository: antono2/vulkan + ref: v3.2.0 + path: build/modules/antono2/vulkan + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + repository: antono2/glfw + ref: v2.0.0 + path: build/modules/antono2/glfw + - name: Install Linux build and rendering tools + if: runner.os == 'Linux' + run: sudo apt-get update && sudo apt-get install -y build-essential cmake libglfw3-dev libvulkan-dev libvulkan-volk-dev libdbus-1-dev pkg-config mesa-vulkan-drivers vulkan-validationlayers xvfb patchelf python3-gi python3-dbus gir1.2-atspi-2.0 xdotool + - name: Install Windows Vulkan build tools + if: runner.os == 'Windows' + shell: bash + run: bash build/modules/antono2/imgui/.github/scripts/setup-vulkan-ci.sh 1.4.309.0 v1.2 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + if: runner.os == 'Windows' + with: + repository: zeux/volk + ref: 58689c063427f5bad4f133625049b1a3c5dd8287 + path: build/volk + - name: Install Windows Volk header + if: runner.os == 'Windows' + shell: pwsh + run: | + $Destination = Join-Path $env:VULKAN_SDK 'Include/volk' + New-Item -ItemType Directory -Force $Destination | Out-Null + Copy-Item build/volk/volk.h, build/volk/volk.c $Destination + - name: Build, bundle and render Linux releases + if: runner.os == 'Linux' + run: bash scripts/build_release_linux.sh '${{ matrix.variant }}' + - name: Exercise bundled gallery through native AT-SPI + if: runner.os == 'Linux' + run: dbus-run-session -- xvfb-run -a /usr/bin/python3 tests/desktop/accessibility.py "build/v-imgui-examples-linux-x64-${{ matrix.variant }}/widget_gallery" "build/release-atspi-${{ matrix.variant }}.log" + - name: Build and audit Windows release DLLs + if: runner.os == 'Windows' + shell: pwsh + run: scripts/build_release_windows.ps1 -Variant '${{ matrix.variant }}' + - name: Upload portable desktop package + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: desktop-${{ runner.os }}-${{ matrix.variant }} + path: build/v-imgui-examples-*.zip + if-no-files-found: error + - name: Upload rendering logs + if: always() && runner.os == 'Linux' + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: render-${{ matrix.variant }} + path: build/v-imgui-examples-linux-*/smoke-logs + + android: + strategy: + fail-fast: false + matrix: + abi: [armeabi-v7a, arm64-v8a, x86_64] + runs-on: ubuntu-24.04 + timeout-minutes: 45 + env: + ANDROID_ABI: ${{ matrix.abi }} + VIMGUI_ANDROID_APPLICATION_UI: '1' + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + - uses: ./.github/actions/setup-v + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '17' + - uses: android-actions/setup-android@v3 + with: + packages: platform-tools + - name: Install Android build tools + run: | + sdkmanager --install 'ndk;27.3.13750724' 'platforms;android-36' 'build-tools;36.0.0' + echo "ANDROID_NDK_HOME=$ANDROID_SDK_ROOT/ndk/27.3.13750724" >> "$GITHUB_ENV" + - name: Build accessible touch app + run: bash scripts/build_android.sh --build-only + - name: Collect Android notices + run: | + case "$ANDROID_ABI" in + armeabi-v7a) target=armv7-linux-androideabi ;; + arm64-v8a) target=aarch64-linux-android ;; + x86_64) target=x86_64-linux-android ;; + esac + python3 scripts/collect_licenses.py build/modules/antono2/imgui "build/android-touch-$ANDROID_ABI/licenses" --target "$target" + cp packaging/ROBOTO-LICENSE.txt "build/android-touch-$ANDROID_ABI/licenses/roboto-license.txt" + - name: Upload unsigned APK and notices for release signing + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: android-release-input-${{ matrix.abi }} + path: | + build/android-touch-${{ matrix.abi }}/vimgui-demo-unsigned.apk + build/android-touch-${{ matrix.abi }}/licenses + if-no-files-found: error diff --git a/.gitignore b/.gitignore index d9f3940..9162275 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,5 @@ tmp.txt # Reproducible dependency checkouts and build artifacts build/ *.apk + +__pycache__/ diff --git a/IMGUI_REVISION b/IMGUI_REVISION index 414802a..8933bdc 100644 --- a/IMGUI_REVISION +++ b/IMGUI_REVISION @@ -1 +1 @@ -a28bfc544a46fd80221bdfe0aa45e161df9782d0 +c093593eeb5685ab4c40b308b9773b51fec37ffe diff --git a/README.md b/README.md index 3648607..3036711 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,15 @@ with Vulkan validation in both standard and docking variants; Windows compiles all desktop examples. Android CI builds debug APKs for three ABIs. Device testing is required before release; see [the testing checklist](docs/testing.md). -## Recommended quick start +## Run a release download + +[Download a release](https://github.com/antono2/v_imgui_examples/releases) and follow +[installation instructions](docs/installing-releases.md). Desktop ZIPs bundle +their application runtime libraries; Android APKs include their native libraries +and font. The gallery and Android app include labelled native accessibility +controls, high contrast and text sizing. + +## Build from source From this checkout, the cross-platform setup installs/builds ImGui and compiles the example without opening a window: @@ -44,8 +52,8 @@ For Android, set the SDK/NDK paths and ABI, then run `scripts/build_android.sh --build-only`. It builds a signed debug APK without installing it. See [Android instructions](examples/android_touch/README.md). -The pinned ImGui commit includes the external-example host options. Until its -upstream PR is merged, use this revision rather than an older installed module. +The pinned ImGui commit includes the external-example host options. Use this +revision rather than an older installed module. The ImGui repository owns native-library setup and pins a tested revision of this example. On Ubuntu or Debian, the shortest supported path is: @@ -106,7 +114,8 @@ the upstream native host. `examples/glfw_vulkan/` contains the shared desktop loop, native flags, and the default demo UI. ImPlot context/spec objects are created and destroyed through host lifecycle callbacks. -iOS/Metal and the optional application/accessibility layer are planned additions. -They are not yet examples in this repository. +The release gallery and touch app integrate the application/accessibility layer. +Raw API views retain ordinary ImGui widgets; they do not automatically expose +screen-reader semantics. iOS/Metal remains an upstream source integration. ![V + Vulkan + GLFW + Dear ImGui](Snapshot_glfw_vulkan.png) diff --git a/docs/installing-releases.md b/docs/installing-releases.md new file mode 100644 index 0000000..f75e121 --- /dev/null +++ b/docs/installing-releases.md @@ -0,0 +1,84 @@ +# Installing release downloads + +Download assets from [Releases](https://github.com/antono2/v_imgui_examples/releases). +The downloads include their application libraries. You do not need to install +V, CMake, Rust, GLFW, an Android SDK, or a separate Visual C++ runtime. +Your operating system, graphical desktop and graphics driver still provide the +platform services. The examples use Vulkan; a supported Vulkan graphics driver +is required. + +## Desktop + +Choose a ZIP for your OS and **x64** processor: + +- **Windows:** Windows 10 or 11. Extract the entire ZIP to a writable folder. + Open `widget_gallery.exe`, `implot_dashboard.exe`, or `glfw_vulkan.exe`. + Keep the DLLs beside the executables. +- **Linux:** Ubuntu 24.04 or a compatible newer distribution, with an X11 or + XWayland desktop. Extract the entire ZIP. Run `./run.sh`, or + `./run.sh implot_dashboard` / `./run.sh glfw_vulkan` from the extracted folder. + Keep the `lib` directory beside the executables. If your archive tool drops + executable permissions, run `chmod +x run.sh glfw_vulkan widget_gallery implot_dashboard`. + +**Docking** includes ImGui docking and optional platform viewports. +**Standard** uses independent ImGui windows. Both contain all three examples. +You can move the extracted folder; binaries use package-relative library paths. +`licenses/` and `RUNTIME-LIBRARIES.txt` identify bundled components. + +## Android: installation without a Play Store + +The Android app needs Android 7.0 (API 24) or later and Vulkan 1.0 support. + +1. Download `v-imgui-touch-universal.apk` on the device. It contains ARM 32-bit, + ARM 64-bit, and x86_64 libraries, so you do not need to choose an ABI. + Smaller APKs for each ABI are also available. +2. Open the download. If Android asks, allow **Install unknown apps** for the + browser or file manager used to open this APK, then return to the installer. + Menu names vary by device. You can disable that permission after installation. +3. Install and open **V ImGui Touch Examples**. Everything needed by the app, + including native libraries and its font, is in the APK. + +To update, install a later release APK over the existing app. Published releases +use the same signing identity. Builds from CI are developer test APKs signed +with debug keys; to replace one with a release, uninstall the test app first. +Uninstalling removes its application data. A signature conflict is not fixed by +installing extra libraries. + +Optional installation from a computer with Android platform tools: + +```sh +adb install -r v-imgui-touch-universal.apk +``` + +The app has no Internet permission. Clipboard copy uses the whole editable +field, and read displays a preview. Touch scrolling works on static text and +empty space. Navigation bars and the keyboard reserve space automatically. + +## Accessibility in the examples + +The desktop widget gallery opens in **Accessible controls** mode. It includes +labelled input, native button/checkbox/radio actions, keyboard focus, status and +progress, high contrast, 200% text, and a virtual file list with off-screen focus. +The **Raw ImGui widgets** view retains the original API gallery, tables and +popups. Raw ImGui widgets and ImPlot charts do not automatically publish native +screen-reader semantics; use Accessible controls to exercise that layer. + +The Android touch app uses the same application/accessibility layer for its +normal controls. Try TalkBack or your device's accessibility service, keyboard +navigation, high contrast and larger text. The native adapter exposes Unicode +text and selection to assistive technology and forwards edits to the keyboard. +Font coverage depends on the bundled font; Unicode round-trips even when a +particular glyph is unavailable. + +There is no separate accessibility app to install. On Linux the native bridge +uses AT-SPI, on Windows UI Automation, and on Android native accessibility nodes. +See [testing](testing.md) for the verified checks and manual release checklist. + +## Other platforms and verification + +Upstream includes iOS/Metal source integrations. Installable iOS artifacts are +not part of this release: device signing and hardware testing remain necessary. +macOS portable Vulkan packages are also not currently published. + +Compare a downloaded file with `SHA256SUMS.txt` from the same release. On Linux +use `sha256sum `; on Windows use `Get-FileHash -Algorithm SHA256`. diff --git a/docs/releasing.md b/docs/releasing.md new file mode 100644 index 0000000..d9f84c4 --- /dev/null +++ b/docs/releasing.md @@ -0,0 +1,35 @@ +# Creating release assets + +Run the **Portable release packages** workflow on the intended examples commit. +It builds both pinned ImGui variants, bundles all non-OS desktop libraries, +audits runtime dependencies, renders the Linux bundles and builds unsigned +Android inputs for three ABIs. Download the artifacts from that exact run. +Retain the run URL and source commits in release notes. Do not publish CI debug +APKs as release updates: their signing keys are temporary. + +Desktop ZIPs are ready to promote after reviewing the checks. Android APKs are +signed outside CI with the persistent private release key: + +```sh +python3 scripts/sign_android_release.py \ + --input /path/to/android-release-input-armeabi-v7a \ + --input /path/to/android-release-input-arm64-v8a \ + --input /path/to/android-release-input-x86_64 \ + --output /path/to/release-assets \ + --build-tools "$ANDROID_SDK_ROOT/build-tools/36.0.0" \ + --keystore /private/path/release.keystore \ + --password-file /private/path/password.txt +``` + +The script rejects mismatched common APK content, combines the ABI libraries, +embeds license notices, aligns the APKs, signs them and verifies each signature. +Keep the key and password private and backed up outside the repository. Never +upload them as release assets. Use the same signing identity for updates. +Increment `android:versionCode` and update `android:versionName` for each release. + +Test the final universal APK on a device, including the installation/update +path. Review [the testing checklist](testing.md) and package instructions. +Generate `SHA256SUMS.txt` from the final ZIP and APK files. Tag the verified +commits only after checks pass, then publish the assets and installation link. +The upstream imgui release has a docking tag and a companion `-standard` source +tag; the same example packages can be attached to both repositories' releases. diff --git a/docs/testing.md b/docs/testing.md index 94160c6..c7f384b 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -56,3 +56,18 @@ adb logcat -s vimgui-android-demo:I AndroidRuntime:E Release only after reviewing CI results and recording the interactive results. No physical-device or assistive-technology coverage is implied by compilation. + +## Integrated accessibility release checks + +The portable workflow renders each Linux bundle after copying its dependencies +and exercises the gallery through the real AT-SPI bus, including native roles, +button actions, status, high contrast, 200% text, off-screen list focus/selection +and view switching. Windows bundles are checked for missing imported runtime +DLLs. Graphical Windows/UI Automation behavior still needs a Windows user session. + +On the connected Android tablet, the integrated accessibility build passed native +roles/actions, checkbox state, progress ranges, Unicode text and UTF-16 selection, +full-field clipboard copy/read and feedback, zoom, scrolling from static text, +and resume state. Repeat with the final release APK and test portrait/landscape, +visible system navigation bars and IME. Check TalkBack spoken navigation manually; +provider action tests do not establish the quality of spoken announcements. diff --git a/examples/android_touch/AndroidManifest.xml b/examples/android_touch/AndroidManifest.xml index 76f6bf1..a26edc0 100644 --- a/examples/android_touch/AndroidManifest.xml +++ b/examples/android_touch/AndroidManifest.xml @@ -1,6 +1,7 @@ + package="io.antono2.vimgui.examples.touch" + android:versionCode="1" android:versionName="1.0.0"> diff --git a/examples/android_touch/README.md b/examples/android_touch/README.md index 8169d10..d53b93c 100644 --- a/examples/android_touch/README.md +++ b/examples/android_touch/README.md @@ -37,3 +37,20 @@ point. Inline marked-text styling and candidate geometry remain upstream limits. Follow [the release checklist](../../docs/testing.md). APK creation and symbol checks do not establish correct behavior on a real keyboard or device. + +## Accessible build and release installation + +Release APKs use the application/accessibility layer in this same V UI. Set +`VIMGUI_ANDROID_APPLICATION_UI=1` before running the build script to enable +labelled native accessibility controls, high contrast and the touch-sized theme. +This build also needs Rust and the selected Android Rust target; the upstream +script prepares the pinned AccessKit sources and builds its native adapter. +The host retains ownership of the input bridge, lifecycle and safe area. + +End users only download and install an APK; follow +[installation without a Play Store](../../docs/installing-releases.md). +No development tools or additional application libraries are needed on the device. +Run `scripts/test_android_accessibility.sh` against the accessible debug build on +a connected device to exercise roles/actions, Unicode selection, full-field +clipboard, text sizing, content swipe and resume. It keeps configured accessibility +services enabled. Human screen-reader navigation remains part of release testing. diff --git a/examples/android_touch/accessibility_d_release_accessibility.v b/examples/android_touch/accessibility_d_release_accessibility.v new file mode 100644 index 0000000..83ec16e --- /dev/null +++ b/examples/android_touch/accessibility_d_release_accessibility.v @@ -0,0 +1,94 @@ +module main + +import math +import antono2.imgui +import antono2.imgui.appui + +#flag -I @VMODROOT/examples/android_touch + +#include "accessibility_state.h" + +fn C.v_imgui_touch_contrast() &bool + +fn C.vimgui_app_input(u64, &char, &char, usize) bool + +// The native host initializes the appui context and Java accessibility adapter. +// The borrowed text/count/zoom buffers retain their existing lifecycle behavior. +fn draw_accessible_touch(zoom &f32, taps &int, text &char, text_capacity int, clipboard &char, clipboard_capacity int) bool { + appui.begin_frame() + storage := imgui.get_state_storage() + base_id := imgui.get_id_str(c'##accessible-base-scale') + mut base_scale := imgui.storage_get_float(storage, base_id, 0) + if base_scale == 0 { + base_scale = imgui.get_style().FontScaleMain / unsafe { *zoom } + imgui.storage_set_float(storage, base_id, base_scale) + } + contrast := C.v_imgui_touch_contrast() + appui.text(2, 'V ImGui: touch and text') + appui.text(3, 'Swipe text or empty space to scroll. Use touch, a keyboard, or your screen reader.') + appui.checkbox(4, 'High contrast', contrast) + if appui.button(5, 'Tap counter') { + unsafe { *taps += 1 } + } + count := 'Taps: ${unsafe { *taps }}' + appui.status(6, count) + appui.progress(7, 'Next hundred taps', f32(unsafe { *taps } % 100) / 100, count) + unsafe { count.free() } + appui.set_width(-1) + status_id := imgui.get_id_str(c'##clipboard-status') + if C.vimgui_app_input(10, c'Editable text', text, usize(text_capacity)) { + imgui.storage_set_int(storage, status_id, 0) + } + appui.text(11, 'Copy uses the entire editable field; no selection is needed. Read displays the clipboard below.') + if appui.button(12, 'Copy all text') { + if unsafe { text[0] == 0 } { + imgui.storage_set_int(storage, status_id, 1) + } else { + imgui.set_clipboard_text(text) + source := imgui.get_clipboard_text() + copy_preview(clipboard, clipboard_capacity, source) + imgui.storage_set_int(storage, status_id, if same_text(text, source) { 2 } else { 3 }) + } + } + if appui.button(13, 'Read clipboard') { + copy_preview(clipboard, clipboard_capacity, imgui.get_clipboard_text()) + imgui.storage_set_int(storage, status_id, if unsafe { clipboard[0] == 0 } { 4 } else { 5 }) + } + status := match imgui.storage_get_int(storage, status_id, 0) { + 1 { 'The field is empty. Enter text before copying.' } + 2 { 'Copied all text. The clipboard preview is shown below.' } + 3 { 'Copy could not be confirmed. Try again while the app is in the foreground.' } + 4 { 'The clipboard is empty or unavailable.' } + 5 { 'Read clipboard. The preview is shown below.' } + 6 { 'Preview cleared. The system clipboard is unchanged.' } + else { '' } + } + appui.status(14, status) + appui.text(15, 'Clipboard preview:') + preview := unsafe { cstring_to_vstring(clipboard) } + appui.text(16, preview) + unsafe { preview.free() } + if appui.button(17, 'Clear preview') { + unsafe { clipboard[0] = 0 } + imgui.storage_set_int(storage, status_id, 6) + } + appui.separator() + if appui.button(20, 'Smaller text') { + unsafe { *zoom = math.max(f32(0.75), *zoom - f32(0.1)) } + } + appui.same_line_for('Larger text', false) + if appui.button(21, 'Larger text') { + unsafe { *zoom = math.min(f32(2), *zoom + f32(0.1)) } + } + if appui.button(22, 'Reset text size') { + unsafe { *zoom = 1 } + } + scale := 'Text size: ${unsafe { *zoom }:.2f}x' + appui.status(23, scale) + unsafe { scale.free() } + appui.text(24, 'Rotate or resume to check retained text, taps, and text size. Navigation bars and the keyboard reserve their space automatically.') + appui.end_frame() or { panic(err) } + // This view owns its accessible theme, including touch target sizing. + appui.theme(true, unsafe { *contrast }, base_scale * unsafe { *zoom }, true) + return false +} diff --git a/examples/android_touch/accessibility_state.h b/examples/android_touch/accessibility_state.h new file mode 100644 index 0000000..e7c2a41 --- /dev/null +++ b/examples/android_touch/accessibility_state.h @@ -0,0 +1,7 @@ +#ifndef V_IMGUI_TOUCH_ACCESSIBILITY_STATE_H +#define V_IMGUI_TOUCH_ACCESSIBILITY_STATE_H +// The UI DSO stays loaded across context/window recreation. Only the render +// thread reads this retained preference; process restart restores the default. +static bool v_imgui_touch_high_contrast = false; +static bool *v_imgui_touch_contrast(void) { return &v_imgui_touch_high_contrast; } +#endif diff --git a/examples/android_touch/ui.v b/examples/android_touch/ui.v index 738064f..48aaf40 100644 --- a/examples/android_touch/ui.v +++ b/examples/android_touch/ui.v @@ -26,6 +26,9 @@ fn edit_text(mut data imgui.InputTextCallbackData) i32 { // It calls this export between NewFrame and Render on its rendering thread. @[export: 'vimgui_android_demo_draw_ui'] fn draw_ui(zoom &f32, taps &int, text &char, text_capacity int, clipboard &char, clipboard_capacity int, width f32, height f32) bool { + $if release_accessibility ? { + return draw_accessible_touch(zoom, taps, text, text_capacity, clipboard, clipboard_capacity) + } imgui.set_next_window_pos(imgui.get_main_viewport().WorkPos, imgui.Cond(imgui.Cond_.always), imgui.ImVec2_c{}) imgui.set_next_window_size(imgui.ImVec2_c{ x: width, y: height }, imgui.Cond(imgui.Cond_.always)) flags := imgui.WindowFlags(int(imgui.WindowFlags_.no_move) | int(imgui.WindowFlags_.no_resize) | int(imgui.WindowFlags_.no_collapse)) diff --git a/examples/glfw_vulkan/main.c.v b/examples/glfw_vulkan/main.c.v index aa73067..8b08f0c 100644 --- a/examples/glfw_vulkan/main.c.v +++ b/examples/glfw_vulkan/main.c.v @@ -51,7 +51,7 @@ pub fn run(options Options, frame fn (mut App, voidptr), state voidptr) { panic('GLFW: Vulkan Not Supported') } - mut app := App{} + mut app := App{ native_window: voidptr(window) } mut extensions := []&char{} mut extensions_count := u32(0) @@ -196,6 +196,7 @@ pub fn run(options Options, frame fn (mut App, voidptr), state voidptr) { pub struct App { pub mut: + native_window voidptr allocator &vk.AllocationCallbacks = unsafe { nil } instance vk.Instance physical_device vk.PhysicalDevice diff --git a/examples/widget_gallery/README.md b/examples/widget_gallery/README.md index 574738a..9326991 100644 --- a/examples/widget_gallery/README.md +++ b/examples/widget_gallery/README.md @@ -19,3 +19,17 @@ resize table columns; toggle the upstream demo. Docking is optional and the same source runs against the standard ImGui variant. ![widget gallery](screenshot.png) + +## Accessible release view + +The portable release builds this same example with `-d release_accessibility +-d appui_embedded` and a native library configured with `VIMGUI_APPLICATION_UI`. +It starts with labelled native accessibility controls, high contrast, 200% text, +status/progress, and a virtual file list. Focus last file reveals the list and +moves keyboard/native focus to its last row. Switch to Raw ImGui widgets for +the original tables, popups and secondary windows. See +[release installation](../../docs/installing-releases.md) for supported downloads. +Use `scripts/build_release_linux.sh docking` or +`scripts/build_release_windows.ps1 -Variant docking` to build the accessible +package from prepared dependencies (Windows uses a Visual Studio developer shell). +The normal setup command remains the direct raw API build. diff --git a/examples/widget_gallery/accessibility_bridge.h b/examples/widget_gallery/accessibility_bridge.h new file mode 100644 index 0000000..c20a6e7 --- /dev/null +++ b/examples/widget_gallery/accessibility_bridge.h @@ -0,0 +1,32 @@ +#ifndef V_IMGUI_GALLERY_ACCESSIBILITY_H +#define V_IMGUI_GALLERY_ACCESSIBILITY_H +#include "vimgui_app.h" +#ifndef GLFW_INCLUDE_NONE +#define GLFW_INCLUDE_NONE +#endif +#include +#ifdef _WIN32 +#define GLFW_EXPOSE_NATIVE_WIN32 +#include +#endif +static bool v_imgui_gallery_attach(void *handle) { + GLFWwindow *window = (GLFWwindow *)handle; + void *native = NULL; +#ifdef _WIN32 + native = glfwGetWin32Window(window); +#endif + return vimgui_accessibility_attach(vimgui_app_accessibility(), native, NULL); +} +static void v_imgui_gallery_update(void *handle) { + GLFWwindow *window = (GLFWwindow *)handle; + int x, y, width, height; + glfwGetWindowPos(window, &x, &y); + glfwGetWindowSize(window, &width, &height); + vimgui_accessibility_window_state(vimgui_app_accessibility(), + glfwGetWindowAttrib(window, GLFW_FOCUSED) != 0, x, y, width, height); + vimgui_accessibility_update(vimgui_app_accessibility()); +} +static void v_imgui_gallery_detach(void) { + vimgui_accessibility_detach(vimgui_app_accessibility()); +} +#endif diff --git a/examples/widget_gallery/accessibility_d_release_accessibility.v b/examples/widget_gallery/accessibility_d_release_accessibility.v new file mode 100644 index 0000000..f306fcb --- /dev/null +++ b/examples/widget_gallery/accessibility_d_release_accessibility.v @@ -0,0 +1,111 @@ +module main + +import examples.glfw_vulkan +import math +import antono2.imgui +import antono2.imgui.appui + +#flag -I @VMODROOT/examples/widget_gallery + +#include "accessibility_bridge.h" + +fn C.v_imgui_gallery_attach(voidptr) bool + +fn C.v_imgui_gallery_update(voidptr) + +fn C.v_imgui_gallery_detach() + +fn C.vimgui_app_input(u64, &char, &char, usize) bool + +fn draw_accessible(mut app glfw_vulkan.App, mut state Gallery) { + if !state.accessible_initialized { + appui.initialize('Widget gallery') or { panic(err) } + if !C.v_imgui_gallery_attach(app.native_window) { + panic('Could not attach native accessibility adapter') + } + appui.list_reset(40) + for i in 0 .. 1000 { + appui.list_add(40, u64(1000 + i), 'Photo ${i:04}.jpg') or { panic(err) } + } + appui.theme(true, false, 1, false) + state.accessible_initialized = true + } + appui.begin_frame() + if state.raw_widgets && appui.back_requested() { state.raw_widgets = false } + appui.text(2, 'Widget gallery') + appui.text(3, 'Use Tab and arrow keys, or your screen reader. Controls have stable names and native actions.') + if appui.radio(4, 'Accessible controls', !state.raw_widgets) { + state.raw_widgets = false + } + appui.same_line_for('Raw ImGui widgets', true) + if appui.radio(5, 'Raw ImGui widgets', state.raw_widgets) { + state.raw_widgets = true + } + mut theme_changed := appui.checkbox(6, 'High contrast', &state.high_contrast) + appui.same_line_for('200% text', true) + theme_changed = appui.checkbox(7, '200% text', &state.large_text) || theme_changed + if state.raw_widgets { + appui.text(8, 'The raw widget view demonstrates the ImGui API. These controls do not publish screen-reader semantics; use Accessible controls for native accessibility.') + draw_widgets(mut app, mut state) + } else { + appui.begin_columns(50, 240 * imgui.get_style().FontScaleMain, 0) + appui.set_width(-1) + C.vimgui_app_input(10, c'Name', &state.name[0], usize(state.name.len)) + appui.next_column(true) + appui.checkbox(11, 'Enabled', &state.enabled) + appui.end_columns() + appui.text(12, 'Priority') + for index, label in ['Low', 'Medium', 'High'] { + if index > 0 { appui.same_line_for(label, true) } + if appui.radio(u64(13 + index), label, state.choice == index) { + state.choice = index + } + } + if appui.button(16, 'Decrease amount') { + state.amount = math.max(f32(0), state.amount - f32(0.1)) + } + appui.same_line_for('Increase amount', false) + if appui.button(17, 'Increase amount') { + state.amount = math.min(f32(1), state.amount + f32(0.1)) + } + appui.progress(18, 'Amount', state.amount, '${int(state.amount * 100)}%') + appui.progress(19, 'Unknown progress', -1, 'Waiting for work') + if appui.button(20, 'Count') { state.count++ } + appui.status(21, 'Count: ${state.count}') + if appui.button(22, 'Copy name') { + imgui.set_clipboard_text(&state.name[0]) + state.clipboard_status = if unsafe { state.name[0] == 0 } { + 'Copied an empty name.' + } else { + 'Copied the name to the clipboard.' + } + } + appui.status(23, state.clipboard_status) + appui.separator() + appui.text(30, 'File list: keyboard arrows and screen-reader actions select a row; focus can reach off-screen rows.') + if appui.button(31, 'Focus last file') { + appui.reveal(40) + appui.focus(1999) + } + activated := appui.list(40, 'Files', state.selected_file, 240) + if activated != 0 { + state.selected_file = activated + } + appui.status(41, if state.selected_file == 0 { + 'No file selected.' + } else { + 'Selected Photo ${state.selected_file - 1000:04}.jpg' + }) + } + appui.end_frame() or { panic(err) } + if theme_changed { + appui.theme(true, state.high_contrast, if state.large_text { f32(2) } else { f32(1) }, false) + } + if state.raw_widgets { draw_auxiliary(mut state) } + C.v_imgui_gallery_update(app.native_window) +} + +fn close_accessibility() { + C.v_imgui_gallery_detach() + appui.shutdown() +} diff --git a/examples/widget_gallery/main.v b/examples/widget_gallery/main.v index d9c8c0e..02ccb70 100644 --- a/examples/widget_gallery/main.v +++ b/examples/widget_gallery/main.v @@ -5,72 +5,42 @@ import antono2.imgui struct Gallery { mut: - name [128]char - enabled bool = true - amount f32 = 0.5 - choice int - count int - show_demo bool - show_details bool + name [128]char + enabled bool = true + amount f32 = 0.5 + choice int + count int + show_demo bool + show_details bool + accessible_initialized bool + raw_widgets bool + high_contrast bool + large_text bool + selected_file u64 + clipboard_status string } fn main() { mut gallery := Gallery{} - glfw_vulkan.run(glfw_vulkan.Options{ title: 'V ImGui: widget gallery' }, draw, &gallery) + glfw_vulkan.run(glfw_vulkan.Options{ title: 'V ImGui: widget gallery', shutdown: close_gallery }, draw, &gallery) } fn draw(mut app glfw_vulkan.App, userdata voidptr) { mut state := unsafe { &Gallery(userdata) } + $if release_accessibility ? { + draw_accessible(mut app, mut state) + return + } imgui.set_next_window_size(imgui.ImVec2{ x: 640, y: 620 }, imgui.Cond(imgui.Cond_.first_use_ever)) if imgui.begin(c'Widget gallery', unsafe { nil }, 0) { - imgui.text_unformatted(c'Each widget edits persistent V state.', unsafe { nil }) - imgui.input_text(c'Name', &state.name[0], usize(state.name.len), 0, unsafe { nil }, unsafe { nil }) - imgui.checkbox(c'Enabled', &state.enabled) - imgui.slider_float(c'Amount', &state.amount, 0, 1, c'%.2f', 0) - imgui.progress_bar(state.amount, imgui.ImVec2{ x: -1 }, c'Slider value') - choices := [c'Low', c'Medium', c'High'] - if imgui.begin_combo(c'Priority', choices[state.choice], 0) { - for index, label in choices { - if imgui.selectable_bool(label, index == state.choice, 0, imgui.ImVec2{}) { - state.choice = index - } - } - imgui.end_combo() - } - if imgui.button(c'Count', imgui.ImVec2{}) { state.count++ } - imgui.same_line(0, -1) - count := 'Count: ${state.count}' - imgui.text_unformatted(count.str, unsafe { nil }) - if imgui.button(c'Open popup', imgui.ImVec2{}) { imgui.open_popup_str(c'Example popup', 0) } - if imgui.begin_popup(c'Example popup', 0) { - imgui.text_unformatted(c'Popups open from an event and keep their own visibility.', unsafe { nil }) - if imgui.button(c'Close popup', imgui.ImVec2{}) { imgui.close_current_popup() } - imgui.end_popup() - } - imgui.separator() - flags := imgui.TableFlags(int(imgui.TableFlags_.borders) | int(imgui.TableFlags_.row_bg) | int(imgui.TableFlags_.resizable)) - if imgui.begin_table(c'Widgets', 2, flags, imgui.ImVec2{}, 0) { - imgui.table_setup_column(c'Widget', 0, 0, 0) - imgui.table_setup_column(c'Use', 0, 0, 0) - imgui.table_headers_row() - labels := [c'InputText', c'Checkbox', c'SliderFloat', c'BeginCombo'] - uses := [c'Edit a fixed buffer', c'Toggle a bool', c'Edit a bounded value', - c'Select one item'] - for i, label in labels { - imgui.table_next_row(0, 0) - imgui.table_next_column() - imgui.text_unformatted(label, unsafe { nil }) - imgui.table_next_column() - imgui.text_unformatted(uses[i], unsafe { nil }) - } - imgui.end_table() - } - imgui.checkbox(c'Details window', &state.show_details) - imgui.checkbox(c'Upstream demo', &state.show_demo) - if app.docking_available { imgui.checkbox(c'Main dockspace', &app.dockspace_enabled) } + draw_widgets(mut app, mut state) } // End is required even when Begin reports a collapsed window. imgui.end() + draw_auxiliary(mut state) +} + +fn draw_auxiliary(mut state Gallery) { if state.show_details { if imgui.begin(c'Details', &state.show_details, 0) { imgui.text_unformatted(c'Your name:', unsafe { nil }) @@ -81,3 +51,57 @@ fn draw(mut app glfw_vulkan.App, userdata voidptr) { } if state.show_demo { imgui.show_demo_window(&state.show_demo) } } + +fn draw_widgets(mut app glfw_vulkan.App, mut state Gallery) { + imgui.text_unformatted(c'Each widget edits persistent V state.', unsafe { nil }) + imgui.input_text(c'Name', &state.name[0], usize(state.name.len), 0, unsafe { nil }, unsafe { nil }) + imgui.checkbox(c'Enabled', &state.enabled) + imgui.slider_float(c'Amount', &state.amount, 0, 1, c'%.2f', 0) + imgui.progress_bar(state.amount, imgui.ImVec2{ x: -1 }, c'Slider value') + choices := [c'Low', c'Medium', c'High'] + if imgui.begin_combo(c'Priority', choices[state.choice], 0) { + for index, label in choices { + if imgui.selectable_bool(label, index == state.choice, 0, imgui.ImVec2{}) { + state.choice = index + } + } + imgui.end_combo() + } + if imgui.button(c'Count', imgui.ImVec2{}) { state.count++ } + imgui.same_line(0, -1) + count := 'Count: ${state.count}' + imgui.text_unformatted(count.str, unsafe { nil }) + if imgui.button(c'Open popup', imgui.ImVec2{}) { imgui.open_popup_str(c'Example popup', 0) } + if imgui.begin_popup(c'Example popup', 0) { + imgui.text_unformatted(c'Popups open from an event and keep their own visibility.', unsafe { nil }) + if imgui.button(c'Close popup', imgui.ImVec2{}) { imgui.close_current_popup() } + imgui.end_popup() + } + imgui.separator() + flags := imgui.TableFlags(int(imgui.TableFlags_.borders) | int(imgui.TableFlags_.row_bg) | int(imgui.TableFlags_.resizable)) + if imgui.begin_table(c'Widgets', 2, flags, imgui.ImVec2{}, 0) { + imgui.table_setup_column(c'Widget', 0, 0, 0) + imgui.table_setup_column(c'Use', 0, 0, 0) + imgui.table_headers_row() + labels := [c'InputText', c'Checkbox', c'SliderFloat', c'BeginCombo'] + uses := [c'Edit a fixed buffer', c'Toggle a bool', c'Edit a bounded value', + c'Select one item'] + for i, label in labels { + imgui.table_next_row(0, 0) + imgui.table_next_column() + imgui.text_unformatted(label, unsafe { nil }) + imgui.table_next_column() + imgui.text_unformatted(uses[i], unsafe { nil }) + } + imgui.end_table() + } + imgui.checkbox(c'Details window', &state.show_details) + imgui.checkbox(c'Upstream demo', &state.show_demo) + if app.docking_available { imgui.checkbox(c'Main dockspace', &app.dockspace_enabled) } +} + +fn close_gallery(_ voidptr) { + $if release_accessibility ? { + close_accessibility() + } +} diff --git a/packaging/MICROSOFT-RUNTIME-NOTICE.txt b/packaging/MICROSOFT-RUNTIME-NOTICE.txt new file mode 100644 index 0000000..6de72b1 --- /dev/null +++ b/packaging/MICROSOFT-RUNTIME-NOTICE.txt @@ -0,0 +1,7 @@ +This package includes the Microsoft Visual C++ x64 Redistributable runtime +DLLs deployed beside the applications from Visual Studio's Redist directory. +Copyright Microsoft Corporation. All rights reserved. +Redistribution is governed by the Microsoft Visual Studio license and its +redistributable code list: +https://visualstudio.microsoft.com/license-terms/ +https://learn.microsoft.com/en-us/visualstudio/releases/2022/redistribution diff --git a/packaging/README-linux.txt b/packaging/README-linux.txt new file mode 100644 index 0000000..415b4b0 --- /dev/null +++ b/packaging/README-linux.txt @@ -0,0 +1,19 @@ +V ImGui examples — Ubuntu 24.04 or newer, x86_64 + +Extract the whole archive. From its directory, run: + ./run.sh widget_gallery + ./run.sh implot_dashboard + ./run.sh glfw_vulkan + +The examples, ImGui/ImPlot library, GLFW, Vulkan loader, C++ runtime, and +non-system runtime dependencies are included. No V compiler, Vulkan SDK, +or additional application libraries need to be installed. + +A graphical X11/XWayland desktop and a Vulkan-capable graphics driver are +required. The graphics driver and base OS (including glibc) remain supplied +by your operating system. This is not a package for a headless server. + +Keep the lib/ directory beside the executables. Paths inside the archive +are relative, so the extracted directory can be moved. VARIANT.txt names +the ImGui variant; RUNTIME-LIBRARIES.txt lists the bundled libraries. +Third-party license notices are in licenses/. diff --git a/packaging/README-windows.txt b/packaging/README-windows.txt new file mode 100644 index 0000000..e0d0d75 --- /dev/null +++ b/packaging/README-windows.txt @@ -0,0 +1,15 @@ +V ImGui examples — Windows 10/11, x64 + +Extract the whole archive and open: + widget_gallery.exe + implot_dashboard.exe + glfw_vulkan.exe + +ImGui/ImPlot, GLFW, the Vulkan loader, and the Microsoft Visual C++ runtime +DLLs are included beside the applications. No compiler, SDK, or separate +Visual C++ runtime installation is needed. Keep the whole folder together. + +A Vulkan-capable graphics driver and a graphical Windows desktop are +required. Windows system DLLs and the device's graphics driver are supplied +by the operating system. VARIANT.txt identifies the ImGui variant. +Third-party license notices are in licenses/. diff --git a/packaging/ROBOTO-LICENSE.txt b/packaging/ROBOTO-LICENSE.txt new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/packaging/ROBOTO-LICENSE.txt @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packaging/VULKAN-LOADER-LICENSE.txt b/packaging/VULKAN-LOADER-LICENSE.txt new file mode 100644 index 0000000..6599e31 --- /dev/null +++ b/packaging/VULKAN-LOADER-LICENSE.txt @@ -0,0 +1,207 @@ +The majority of files in this project use the Apache 2.0 License. +There are a few exceptions and their license can be found in the source. +Any license deviations from Apache 2.0 are "more permissive" licenses. + +=========================================================================================== + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/scripts/build_android.sh b/scripts/build_android.sh index dd5d87e..27de250 100755 --- a/scripts/build_android.sh +++ b/scripts/build_android.sh @@ -19,7 +19,7 @@ if [[ "$(git -C "$imgui_dir" rev-parse HEAD)" != "$revision" ]]; then exit 2 fi git -C "$imgui_dir" submodule update --init --recursive -export VIMGUI_ANDROID_UI_SOURCE="$repo_dir/examples/android_touch/ui.v" +export VIMGUI_ANDROID_UI_SOURCE="$repo_dir/examples/android_touch" export VIMGUI_ANDROID_MANIFEST="$repo_dir/examples/android_touch/AndroidManifest.xml" export VIMGUI_ANDROID_BUILD_DIR="${VIMGUI_ANDROID_BUILD_DIR:-$repo_dir/build/android-touch-${ANDROID_ABI:-device}}" exec bash "$imgui_dir/scripts/run_android_demo.sh" "$mode" diff --git a/scripts/build_release_linux.sh b/scripts/build_release_linux.sh new file mode 100755 index 0000000..73ecb28 --- /dev/null +++ b/scripts/build_release_linux.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +set -euo pipefail +root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) +imgui=${IMGUI_DIR:-$root/build/modules/antono2/imgui} +variant=${1:?Usage: build_release_linux.sh docking|standard} +export VMODULES=${VMODULES:-$root/build/modules} +export VULKAN_SDK=/usr GLFW_INCLUDE=/usr/include GLFW_LIB=/usr/lib/x86_64-linux-gnu +v=${V_BIN:-v} +python3 "$imgui/scripts/prepare-accesskit.py" +"$v" run "$imgui/build_vimgui.vsh" --linkage shared --glfw system +accesskit="$imgui/.dependencies/accesskit/accesskit-c-0.23.1" +cmake -S "$imgui" -B "$imgui/build/shared-system" \ + -DVIMGUI_APPLICATION_UI=ON "-DACCESSKIT_DIR=$accesskit" \ + "-DVIMGUI_ACCESSKIT_STATIC_LIBRARY=$accesskit/target/release/libaccesskit.a" +cmake --build "$imgui/build/shared-system" --parallel 4 +binaries="$root/build/release-binaries-$variant" +mkdir -p "$binaries" +for example in glfw_vulkan widget_gallery implot_dashboard; do + source=$root + if [[ $example != glfw_vulkan ]]; then source="$root/examples/$example"; fi + "$v" -d release_accessibility -d appui_embedded -no-memory-limit -cc gcc -o "$binaries/$example" "$source" +done +python3 "$root/scripts/package_linux.py" "$binaries" "$imgui" "$root/build/v-imgui-examples-linux-x64-$variant" --variant "$variant" +# Check the extracted bundle from outside its original build directory. +bash "$root/scripts/smoke_desktop.sh" "$root/build/v-imgui-examples-linux-x64-$variant" diff --git a/scripts/build_release_windows.ps1 b/scripts/build_release_windows.ps1 new file mode 100644 index 0000000..3df68ec --- /dev/null +++ b/scripts/build_release_windows.ps1 @@ -0,0 +1,52 @@ +param([Parameter(Mandatory)] [ValidateSet('docking', 'standard')] [string] $Variant) +$ErrorActionPreference = 'Stop' +$Root = Split-Path -Parent $PSScriptRoot +Set-Location $Root +$VsWhere = Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio\Installer\vswhere.exe' +$InstallPath = & $VsWhere -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath +if (-not $InstallPath) { throw 'Visual Studio C++ tools were not found' } +Import-Module (Join-Path $InstallPath 'Common7\Tools\Microsoft.VisualStudio.DevShell.dll') +Enter-VsDevShell -VsInstallPath $InstallPath -SkipAutomaticLocation -DevCmdArguments '-arch=x64 -host_arch=x64' + +$ImguiRoot = Join-Path $env:GITHUB_WORKSPACE 'build\modules\antono2\imgui' +python (Join-Path $ImguiRoot 'scripts/prepare-accesskit.py') --prebuilt +if ($LASTEXITCODE -ne 0) { throw 'Could not prepare AccessKit' } +v run (Join-Path $ImguiRoot 'build_vimgui.vsh') --linkage shared --glfw bundled --glfw-version 3.4 +if ($LASTEXITCODE -ne 0) { throw 'Could not build the native ImGui library' } + +$NativeBuild = Join-Path $ImguiRoot 'build\shared-bundled-3.4' +cmake -S $ImguiRoot -B $NativeBuild -DVIMGUI_APPLICATION_UI=ON "-DACCESSKIT_DIR=$ImguiRoot/.dependencies/accesskit/accesskit-c-0.23.1" +if ($LASTEXITCODE -ne 0) { throw 'Accessible native configure failed' } +cmake --build $NativeBuild --config Release --parallel 4 +if ($LASTEXITCODE -ne 0) { throw 'Accessible native build failed' } + +$GlfwHeader = Get-ChildItem $NativeBuild -Recurse -Filter 'glfw3.h' | + Where-Object { $_.FullName -match 'glfw-src.*include.GLFW' } | Select-Object -First 1 +$GlfwLibrary = Get-ChildItem $NativeBuild -Recurse -Include 'glfw3dll.lib', 'glfw3.lib' | Select-Object -First 1 +$GlfwDll = Get-ChildItem $NativeBuild -Recurse -Filter 'glfw3.dll' | Select-Object -First 1 +$VimguiDll = Get-ChildItem (Join-Path $ImguiRoot 'lib') -Recurse -Filter 'vimgui.dll' | Select-Object -First 1 +if (-not $GlfwHeader -or -not $GlfwLibrary -or -not $GlfwDll -or -not $VimguiDll) { + throw 'The native build did not produce all required Windows artifacts' +} + +$LinkDirectory = Join-Path $env:RUNNER_TEMP 'link' +New-Item -ItemType Directory -Force $LinkDirectory | Out-Null +Copy-Item $GlfwLibrary.FullName (Join-Path $LinkDirectory 'glfw3.lib') +$env:GLFW_INCLUDE = Split-Path -Parent (Split-Path -Parent $GlfwHeader.FullName) +$env:GLFW_LIB = $LinkDirectory +$Executable = Join-Path $env:RUNNER_TEMP 'v_imgui_demo.exe' +$ModulePath = "$env:GITHUB_WORKSPACE\build\modules\antono2|@vlib|@vmodules" +$Output = Join-Path $Root "build/release-binaries-$Variant" +New-Item -ItemType Directory -Force $Output | Out-Null +foreach ($Example in @('glfw_vulkan', 'widget_gallery', 'implot_dashboard')) { + $Source = if ($Example -eq 'glfw_vulkan') { '.' } else { "./examples/$Example" } + $Executable = Join-Path $Output "$Example.exe" + v -d release_accessibility -d appui_embedded -no-memory-limit -path $ModulePath -cc msvc -o $Executable $Source + if ($LASTEXITCODE -ne 0) { throw "The Windows $Example did not compile" } +} +Copy-Item $GlfwDll.FullName, $VimguiDll.FullName $Output -Force +foreach ($Artifact in @($Executable, $GlfwDll.FullName, $VimguiDll.FullName)) { + if (-not (Test-Path $Artifact)) { throw "Missing Windows artifact: $Artifact" } +} + +& (Join-Path $PSScriptRoot 'package_windows.ps1') -Binaries $Output -Imgui $ImguiRoot -Variant $Variant -Output (Join-Path $Root "build/v-imgui-examples-windows-x64-$Variant") diff --git a/scripts/collect_licenses.py b/scripts/collect_licenses.py new file mode 100644 index 0000000..e0d6ffc --- /dev/null +++ b/scripts/collect_licenses.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 +"""Collect notices for native sources and the pinned AccessKit Rust graph.""" +import argparse +import json +from pathlib import Path +import shutil +import subprocess + +parser = argparse.ArgumentParser(description=__doc__) +parser.add_argument('imgui', type=Path) +parser.add_argument('output', type=Path) +parser.add_argument('--target', required=True) +args = parser.parse_args() +root = Path(__file__).resolve().parent.parent +args.output.mkdir(parents=True, exist_ok=True) +for label, source in { + 'examples': root / 'LICENSE', 'imgui-v': args.imgui / 'LICENSE', + 'imgui': args.imgui / 'cimgui/imgui/LICENSE.txt', + 'cimgui': args.imgui / 'cimgui/LICENSE', + 'implot': args.imgui / 'cimplot/implot/LICENSE', + 'cimplot': args.imgui / 'cimplot/LICENSE', +}.items(): + shutil.copy2(source, args.output / f'{label}.txt') +accesskit = args.imgui / '.dependencies/accesskit/accesskit-c-0.23.1' +metadata = json.loads(subprocess.check_output([ + 'cargo', 'metadata', '--locked', '--format-version', '1', '--filter-platform', args.target, + '--manifest-path', str(accesskit / 'Cargo.toml')], text=True, encoding='utf-8')) +notices = [] +for package in metadata['packages']: + folder = Path(package['manifest_path']).parent + label = f"{package['name']}-{package['version']}" + destination = args.output / 'rust' / label + destination.mkdir(parents=True, exist_ok=True) + notices.append(f"{label}: {package.get('license') or 'See included license file'}") + (destination / 'SOURCE.txt').write_text( + f"{label}\nLicense: {package.get('license')}\nSource: {package.get('repository') or package.get('source') or 'AccessKit release source'}\n") + for file in folder.iterdir(): + if file.is_file() and any(word in file.name.upper() for word in ('LICENSE', 'COPYING', 'NOTICE')): + shutil.copy2(file, destination / file.name) + custom = package.get('license_file') + if custom: + shutil.copy2(folder / custom, destination / Path(custom).name) +(args.output / 'RUST-DEPENDENCIES.txt').write_text('\n'.join(sorted(notices)) + '\n') diff --git a/scripts/package_linux.py b/scripts/package_linux.py new file mode 100755 index 0000000..0173ded --- /dev/null +++ b/scripts/package_linux.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 +"""Stage trusted Linux examples and their non-glibc runtime dependencies.""" +import argparse +import hashlib +import os +from pathlib import Path +import re +import shutil +import subprocess +import zipfile + +BASE = re.compile(r'^(lib(c|m|pthread|dl|rt|resolv|util|mvec)\.so\.|ld-linux|linux-vdso)') +EXAMPLES = ('glfw_vulkan', 'widget_gallery', 'implot_dashboard') + + +def run(*args): + return subprocess.check_output(args, text=True, env={**os.environ, 'LD_LIBRARY_PATH': ''}) + + +def dependencies(path): + result = run('ldd', str(path)) + if 'not found' in result: + raise RuntimeError(f'Unresolved dependency for {path}:\n{result}') + for line in result.splitlines(): + match = re.match(r'\s*(\S+) => (/\S+) ', line) + if match and not BASE.match(match[1]): + yield match[1], Path(match[2]) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('binaries', type=Path) + parser.add_argument('imgui', type=Path) + parser.add_argument('output', type=Path) + parser.add_argument('--variant', choices=['docking', 'standard'], required=True) + args = parser.parse_args() + root = Path(__file__).resolve().parent.parent + package = args.output.resolve() + package.mkdir(parents=True, exist_ok=False) + lib = package / 'lib' + notices = package / 'licenses' + lib.mkdir() + notices.mkdir() + queue = [] + for name in EXAMPLES: + source = args.binaries / name + shutil.copy2(source, package / name) + queue.extend(dependencies(source)) + # GLFW loads these with dlopen(), so they do not all appear in ldd. + for name in ('libvulkan.so.1', 'libXrandr.so.2', 'libXinerama.so.1', + 'libXcursor.so.1', 'libXi.so.6', 'libXxf86vm.so.1'): + source = Path('/usr/lib/x86_64-linux-gnu') / name + if not source.exists(): + raise RuntimeError(f'Missing runtime library: {source}') + queue.append((name, source)) + copied = {} + owners = set() + while queue: + name, source = queue.pop() + digest = hashlib.sha256(source.read_bytes()).hexdigest() + if name in copied: + if copied[name] != digest: + raise RuntimeError(f'Conflicting runtime library: {name}') + continue + copied[name] = digest + shutil.copy2(source, lib / name) + queue.extend(dependencies(source)) + # Debian/Ubuntu copyright files include upstream license notices. + candidates = {str(source), str(source.resolve())} + for candidate in list(candidates): + if candidate.startswith('/usr/lib/'): + candidates.add(candidate.removeprefix('/usr')) + elif candidate.startswith('/lib/'): + candidates.add('/usr' + candidate) + for candidate in candidates: + result = subprocess.run(['dpkg-query', '-S', candidate], text=True, capture_output=True) + if result.returncode == 0: + for line in result.stdout.splitlines(): + owner = line.split(': ', 1)[0].split(':')[0] + copyright_file = Path('/usr/share/doc') / owner / 'copyright' + if copyright_file.exists(): + owners.add(owner) + for owner in owners: + shutil.copy2(Path('/usr/share/doc') / owner / 'copyright', notices / f'{owner}.txt') + for label, source in { + 'examples': root / 'LICENSE', 'imgui-v': args.imgui / 'LICENSE', + 'imgui': args.imgui / 'cimgui/imgui/LICENSE.txt', + 'cimgui': args.imgui / 'cimgui/LICENSE', + 'implot': args.imgui / 'cimplot/implot/LICENSE', + 'cimplot': args.imgui / 'cimplot/LICENSE', + }.items(): + shutil.copy2(source, notices / f'{label}.txt') + subprocess.run(['python3', str(root / 'scripts/collect_licenses.py'), str(args.imgui), str(notices), + '--target', 'x86_64-unknown-linux-gnu'], check=True) + shutil.copy2(root / 'packaging/run.sh', package / 'run.sh') + (package / 'run.sh').chmod(0o755) + shutil.copy2(root / 'packaging/README-linux.txt', package / 'README.txt') + (package / 'VARIANT.txt').write_text(args.variant + '\n') + (package / 'RUNTIME-LIBRARIES.txt').write_text('\n'.join(sorted(copied)) + '\n') + for path in [*(package / name for name in EXAMPLES), *lib.iterdir()]: + subprocess.run(['patchelf', '--set-rpath', '$ORIGIN/lib' if path.parent == package else '$ORIGIN', str(path)], check=True) + for path in [*(package / name for name in EXAMPLES), *lib.iterdir()]: + for name, source in dependencies(path): + if source.parent != lib: + raise RuntimeError(f'{path.name} depends on an unbundled library: {name} => {source}') + archive = package.with_suffix('.zip') + with zipfile.ZipFile(archive, 'w', zipfile.ZIP_DEFLATED, compresslevel=9) as out: + for path in sorted(package.rglob('*')): + if path.is_file(): + out.write(path, path.relative_to(package.parent)) + print(archive) + + +if __name__ == '__main__': + main() diff --git a/scripts/package_windows.ps1 b/scripts/package_windows.ps1 new file mode 100644 index 0000000..590b056 --- /dev/null +++ b/scripts/package_windows.ps1 @@ -0,0 +1,56 @@ +param( + [Parameter(Mandatory)] [string] $Binaries, + [Parameter(Mandatory)] [string] $Imgui, + [Parameter(Mandatory)] [string] $Output, + [Parameter(Mandatory)] [ValidateSet('docking', 'standard')] [string] $Variant +) +$ErrorActionPreference = 'Stop' +$Root = Split-Path -Parent $PSScriptRoot +if (Test-Path $Output) { throw "Output already exists: $Output" } +New-Item -ItemType Directory $Output | Out-Null +$Licenses = Join-Path $Output 'licenses' +foreach ($Name in @('glfw_vulkan.exe', 'widget_gallery.exe', 'implot_dashboard.exe', 'vimgui.dll', 'glfw3.dll')) { + Copy-Item (Join-Path $Binaries $Name) $Output +} +# Deploy the Microsoft redistributable CRT beside the application. +if (-not $env:VCToolsRedistDir) { throw 'Run in the Visual Studio developer shell' } +$Crt = Get-ChildItem (Join-Path $env:VCToolsRedistDir 'x64') -Directory -Filter 'Microsoft.VC*.CRT' | Select-Object -First 1 +if (-not $Crt) { throw 'The redistributable x64 C runtime was not found' } +Get-ChildItem $Crt.FullName -Filter '*.dll' | Copy-Item -Destination $Output +Copy-Item (Join-Path $env:VULKAN_SDK 'Bin/vulkan-1.dll') $Output +python (Join-Path $PSScriptRoot 'collect_licenses.py') $Imgui $Licenses --target x86_64-pc-windows-msvc +if ($LASTEXITCODE -ne 0) { throw 'License collection failed' } +$GlfwLicense = Get-ChildItem (Join-Path $Imgui 'build/shared-bundled-3.4') -Recurse -Filter 'LICENSE.md' | + Where-Object { $_.FullName -match 'glfw-src' } | Select-Object -First 1 +if (-not $GlfwLicense) { throw 'GLFW license not found' } +Copy-Item $GlfwLicense.FullName (Join-Path $Licenses 'glfw.txt') +Copy-Item (Join-Path $Root 'packaging/README-windows.txt') (Join-Path $Output 'README.txt') +Copy-Item (Join-Path $Root 'packaging/MICROSOFT-RUNTIME-NOTICE.txt') $Licenses +Copy-Item (Join-Path $Root 'packaging/VULKAN-LOADER-LICENSE.txt') $Licenses +$Variant | Set-Content (Join-Path $Output 'VARIANT.txt') +# Every imported non-OS DLL must exist beside the applications. Driver DLLs are +# loaded by the Vulkan loader at runtime from the installed graphics driver. +$Imports = @{} +foreach ($File in Get-ChildItem $Output -File | Where-Object { $_.Extension -in '.exe', '.dll' }) { + $Dump = & dumpbin /nologo /dependents $File.FullName + if ($LASTEXITCODE -ne 0) { throw "Cannot inspect $($File.Name)" } + foreach ($Line in $Dump) { + if ($Line -match '^\s+([a-zA-Z0-9_.-]+\.dll)\s*$') { + $Name = $Matches[1].ToLowerInvariant() + $Imports[$Name] = $true + if (Test-Path (Join-Path $Output $Name)) { continue } + $OsDlls = @('kernel32.dll','user32.dll','gdi32.dll','advapi32.dll','shell32.dll','ole32.dll', + 'oleaut32.dll','comdlg32.dll','shlwapi.dll','setupapi.dll','cfgmgr32.dll','ntdll.dll','ucrtbase.dll', + 'ws2_32.dll','bcrypt.dll','crypt32.dll','secur32.dll','rpcrt4.dll','dwmapi.dll','version.dll', + 'userenv.dll','imm32.dll','winmm.dll','msvcrt.dll','powrprof.dll','uxtheme.dll', + 'hid.dll','winspool.drv','uiautomationcore.dll','propsys.dll','netapi32.dll','mswsock.dll', + 'normaliz.dll','d3d11.dll','dxgi.dll','opengl32.dll') + if ($Name -notin $OsDlls -and $Name -notmatch '^(api|ext)-ms-') { + throw "Unbundled runtime dependency: $($File.Name) -> $Name" + } + } + } +} +$Imports.Keys | Sort-Object | Set-Content (Join-Path $Output 'RUNTIME-LIBRARIES.txt') +Compress-Archive -Path $Output -DestinationPath "$Output.zip" -CompressionLevel Optimal +Write-Output "$Output.zip" diff --git a/scripts/sign_android_release.py b/scripts/sign_android_release.py new file mode 100644 index 0000000..f53e7e4 --- /dev/null +++ b/scripts/sign_android_release.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +"""Combine CI ABI inputs and sign installable APKs with a private release key.""" +import argparse +import hashlib +from pathlib import Path +import re +import subprocess +import tempfile +import zipfile + +parser = argparse.ArgumentParser(description=__doc__) +parser.add_argument('--input', type=Path, action='append', required=True, + help='Extracted CI artifact directory with unsigned APK and licenses') +parser.add_argument('--output', type=Path, required=True) +parser.add_argument('--build-tools', type=Path, required=True) +parser.add_argument('--keystore', type=Path, required=True) +parser.add_argument('--password-file', type=Path, required=True) +parser.add_argument('--alias', default='v-imgui-release') +args = parser.parse_args() +args.output.mkdir(parents=True, exist_ok=True) +shared = None +libraries = {} +notices = {} +for folder in args.input: + apk = folder / 'vimgui-demo-unsigned.apk' + with zipfile.ZipFile(apk) as archive: + common = {} + for name in archive.namelist(): + if name.endswith('/') or name.startswith('META-INF/'): continue + data = archive.read(name) + if name.startswith('lib/'): + if not re.fullmatch(r'lib/(armeabi-v7a|arm64-v8a|x86_64)/[^/]+\.so', name): + raise ValueError(f'Unexpected native library path: {name}') + if name in libraries: raise ValueError(f'Duplicate native input: {name}') + libraries[name] = data + else: + common[name] = data + if shared is None: shared = common + elif shared != common: raise ValueError('ABI inputs disagree on the manifest, UI classes or assets') + for file in (folder / 'licenses').rglob('*'): + if not file.is_file(): continue + name = 'assets/licenses/' + file.relative_to(folder / 'licenses').as_posix() + if name in notices and notices[name] != file.read_bytes(): + raise ValueError(f'License inputs disagree: {name}') + notices[name] = file.read_bytes() +abis = {name.split('/')[1] for name in libraries} +if abis != {'armeabi-v7a', 'arm64-v8a', 'x86_64'}: + raise ValueError(f'Expected all three supported ABIs, got {abis}') +for abi in abis: + expected = {f'lib/{abi}/{name}' for name in ('libvimgui.so', 'libvimgui_android_demo.so', 'libvimgui_android_ui.so')} + if {name for name in libraries if name.split('/')[1] == abi} != expected: + raise ValueError(f'Incomplete ABI: {abi}') +with tempfile.TemporaryDirectory() as temporary: + temporary = Path(temporary) + for variant in ['universal', *sorted(abis)]: + unsigned = temporary / 'unsigned.apk' + aligned = temporary / 'aligned.apk' + output = args.output / f'v-imgui-touch-{variant}.apk' + with zipfile.ZipFile(unsigned, 'w', zipfile.ZIP_DEFLATED, compresslevel=9) as archive: + for name, data in sorted({**shared, **notices, **libraries}.items()): + if name.startswith('lib/') and variant != 'universal' and name.split('/')[1] != variant: continue + entry = zipfile.ZipInfo(name, date_time=(2026, 1, 1, 0, 0, 0)) + entry.compress_type = zipfile.ZIP_DEFLATED + archive.writestr(entry, data) + subprocess.run([str(args.build_tools / 'zipalign'), '-f', '-p', '4', str(unsigned), str(aligned)], check=True) + subprocess.run([str(args.build_tools / 'apksigner'), 'sign', '--ks', str(args.keystore), + '--ks-key-alias', args.alias, '--ks-pass', 'file:' + str(args.password_file), + '--key-pass', 'file:' + str(args.password_file), '--out', str(output), str(aligned)], check=True) + subprocess.run([str(args.build_tools / 'apksigner'), 'verify', '--verbose', str(output)], check=True) + print(f'{hashlib.sha256(output.read_bytes()).hexdigest()} {output.name}') diff --git a/scripts/test_android_accessibility.sh b/scripts/test_android_accessibility.sh new file mode 100755 index 0000000..2d89966 --- /dev/null +++ b/scripts/test_android_accessibility.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +# Run against the accessible debug APK produced by build_android.sh. +set -euo pipefail +repo=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) +sdk=${ANDROID_SDK_ROOT:-${ANDROID_HOME:-}} +adb=("$sdk/platform-tools/adb") +if [[ -n ${ANDROID_SERIAL:-} ]]; then adb+=(-s "$ANDROID_SERIAL"); fi +abi=${ANDROID_ABI:-$("${adb[@]}" shell getprop ro.product.cpu.abi | tr -d '\r')} +build=$repo/build/android-touch-$abi +build_tools=$(printf '%s\n' "$sdk"/build-tools/* | sort -V | tail -n 1) +android_jar=$(printf '%s\n' "$sdk"/platforms/*/android.jar | sort -V | tail -n 1) +package=$build/accessibility-test +mkdir -p "$package/classes" "$package/dex" +"${JAVAC:-javac}" -source 8 -target 8 -Xlint:-options -cp "$android_jar" -d "$package/classes" \ + "$repo/tests/android/accessibility/AccessibilitySmoke.java" +mapfile -d '' class_files < <(find "$package/classes" -name '*.class' -print0) +"$build_tools/d8" --min-api 24 --lib "$android_jar" --output "$package/dex" "${class_files[@]}" +"$build_tools/aapt" package -f -M "$repo/tests/android/accessibility/AndroidManifest.xml" \ + -I "$android_jar" -F "$package/unsigned.apk" +(cd "$package/dex"; "$build_tools/aapt" add "$package/unsigned.apk" classes.dex) +"$build_tools/zipalign" -f 4 "$package/unsigned.apk" "$package/aligned.apk" +"$build_tools/apksigner" sign --ks "$build/debug.keystore" --ks-pass pass:android --key-pass pass:android \ + --out "$package/test.apk" "$package/aligned.apk" +"$build_tools/apksigner" verify "$package/test.apk" +"${adb[@]}" shell input keyevent 224 +"${adb[@]}" shell wm dismiss-keyguard +"${adb[@]}" shell am force-stop io.antono2.vimgui.examples.touch +"${adb[@]}" install -r "$build/vimgui-demo-$abi.apk" +"${adb[@]}" install -r "$package/test.apk" +"${adb[@]}" shell am instrument -w \ + io.antono2.vimgui.examples.touch.test/io.antono2.vimgui.examples.touch.test.AccessibilitySmoke | tee "$package/instrumentation.log" +rg -q 'result=PASS:' "$package/instrumentation.log" diff --git a/tests/android/accessibility/AccessibilitySmoke.java b/tests/android/accessibility/AccessibilitySmoke.java new file mode 100644 index 0000000..abb9f4e --- /dev/null +++ b/tests/android/accessibility/AccessibilitySmoke.java @@ -0,0 +1,150 @@ +package io.antono2.vimgui.examples.touch.test; + +import android.app.Activity; +import android.app.Instrumentation; +import android.app.UiAutomation; +import android.accessibilityservice.AccessibilityServiceInfo; +import android.content.Intent; +import android.graphics.Rect; +import android.os.Bundle; +import android.os.SystemClock; +import android.view.View; +import android.view.MotionEvent; +import android.view.InputDevice; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.AccessibilityWindowInfo; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputConnection; +import java.util.ArrayDeque; +import java.util.function.Supplier; +import java.util.concurrent.atomic.AtomicReference; + +/** Exercises the native Android accessibility provider without changing the + * user's configured accessibility services. Touch coordinates come from the app's list bounds. */ +public final class AccessibilitySmoke extends Instrumentation { + private UiAutomation automation; + private String lastTree=""; + + @Override public void onCreate(Bundle arguments) { super.onCreate(arguments); start(); } + + private T await(Supplier probe, String message) { + long deadline=SystemClock.uptimeMillis()+30000; + do { + T value=probe.get(); + if (value!=null) return value; + SystemClock.sleep(100); + } while (SystemClock.uptimeMillis() queue=new ArrayDeque<>(); + StringBuilder trace=new StringBuilder(); + for (AccessibilityWindowInfo window:automation.getWindows()) { + AccessibilityNodeInfo root=window.getRoot(); + trace.append("window=").append(window.getId()).append(" package=").append(root==null?"null":root.getPackageName()).append(';'); + if (root!=null && "io.antono2.vimgui.examples.touch".contentEquals(root.getPackageName()==null?"":root.getPackageName())) queue.add(root); + } + while (!queue.isEmpty()) { + AccessibilityNodeInfo node=queue.removeFirst(); + if (trace.length()<4000) trace.append(node.getClassName()).append(':').append(node.getContentDescription()).append(':').append(node.getText()).append(" children=").append(node.getChildCount()).append(';'); + if (label.contentEquals(node.getContentDescription()==null?"":node.getContentDescription()) || + label.contentEquals(node.getText()==null?"":node.getText())) return node; + for (int i=0;i find(label),"Missing accessible control: "+label); } + private void click(String label) { + if (!node(label).performAction(AccessibilityNodeInfo.ACTION_CLICK)) throw new AssertionError("Click rejected: "+label); + } + private View inputView(View root) { + if (root.getClass().getName().equals("io.antono2.imgui.ImGuiInputView")) return root; + if (root instanceof ViewGroup) { + ViewGroup group=(ViewGroup)root; + for (int i=0;i activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)); + for(String label:new String[]{"V ImGui: touch and text","High contrast","Tap counter","Editable text"}) { + if(!node(label).isImportantForAccessibility())throw new AssertionError("Control skipped by screen readers: "+label); + } + if(!node("High contrast").isCheckable())throw new AssertionError("Checkbox role missing"); + click("High contrast"); + await(() -> node("High contrast").isChecked()?Boolean.TRUE:null,"High contrast did not toggle"); + click("Tap counter"); node("Taps: 1"); + AccessibilityNodeInfo.RangeInfo range=node("Next hundred taps").getRangeInfo(); + if(range==null || Math.abs(range.getCurrent()-.01f)>.001f)throw new AssertionError("Progress range did not update"); + Bundle text=new Bundle(); text.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE,"A📷e\u0301Z"); + if(!node("Editable text").performAction(AccessibilityNodeInfo.ACTION_SET_TEXT,text))throw new AssertionError("Set text rejected"); + await(() -> "A📷e\u0301Z".contentEquals(node("Editable text").getText())?Boolean.TRUE:null,"Unicode text damaged"); + Bundle selection=new Bundle(); + selection.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_START_INT,1); + selection.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_END_INT,3); + if(!node("Editable text").performAction(AccessibilityNodeInfo.ACTION_SET_SELECTION,selection))throw new AssertionError("Selection rejected"); + await(() -> node("Editable text").getTextSelectionStart()==1 && node("Editable text").getTextSelectionEnd()==3?Boolean.TRUE:null,"Selection did not round-trip"); + click("Copy all text"); node("Copied all text. The clipboard preview is shown below."); + click("Clear preview"); node("Preview cleared. The system clipboard is unchanged."); + click("Read clipboard"); node("Read clipboard. The preview is shown below."); node("A📷e\u0301Z"); + click("Larger text"); node("Text size: 1.10x"); + click("Reset text size"); node("Text size: 1.00x"); + // Restore the visible top, then swipe static instructions rather than the scrollbar. + node("Tap counter").performAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SHOW_ON_SCREEN.getId()); + automation.performGlobalAction(android.accessibilityservice.AccessibilityService.GLOBAL_ACTION_BACK); + SystemClock.sleep(500); + Rect before=new Rect();node("Tap counter").getBoundsInScreen(before); + Rect instructions=new Rect();node("Copy uses the entire editable field; no selection is needed. Read displays the clipboard below.").getBoundsInScreen(instructions); + if(instructions.height()>0 && instructions.top>150) { + drag(instructions.centerX(),instructions.centerY(),100); + await(() -> {Rect after=new Rect();node("Tap counter").getBoundsInScreen(after);return after.top + + + + + diff --git a/tests/desktop/accessibility.py b/tests/desktop/accessibility.py new file mode 100644 index 0000000..f7ef1de --- /dev/null +++ b/tests/desktop/accessibility.py @@ -0,0 +1,52 @@ +import gi,sys,time,subprocess,os +gi.require_version('Atspi','2.0') +from gi.repository import Atspi,GLib +import dbus +bus=dbus.SessionBus() +properties=dbus.Interface(bus.get_object("org.a11y.Bus","/org/a11y/bus"),"org.freedesktop.DBus.Properties") +properties.Set("org.a11y.Status","IsEnabled",dbus.Boolean(True)) +properties.Set("org.a11y.Status","ScreenReaderEnabled",dbus.Boolean(True)) +process=subprocess.Popen([sys.argv[1]], stdout=open(sys.argv[2],'w'),stderr=subprocess.STDOUT) +def find(name): + q=[Atspi.get_desktop(0)] + while q: + n=q.pop(0) + try: + if n.get_name()==name: return n + q.extend(n.get_child_at_index(i) for i in range(n.get_child_count())) + except Exception: pass + return None +def await_node(name): + end=time.monotonic()+20 + while time.monotonic()0,name + for i in range(a.get_n_actions()): + if a.get_action_name(i) in ('click','press','activate','toggle'): + assert a.do_action(i);return + raise AssertionError('No click action: '+name) +try: + print('Roles:',[(name,await_node(name).get_role_name()) for name in ('High contrast','Name','Count','Files')],flush=True) + subprocess.run(['xdotool','search','--sync','--name','V ImGui: widget gallery','windowfocus'],check=True) + click('Count');await_node('Count: 1') + click('High contrast');time.sleep(.2) + assert await_node('High contrast').get_state_set().contains(Atspi.StateType.CHECKED) + click('200% text');time.sleep(.2) + click('Focus last file');last=await_node('Photo 0999.jpg') + end=time.monotonic()+10 + while not last.get_state_set().contains(Atspi.StateType.FOCUSED) and time.monotonic() Date: Sat, 3 Oct 2026 05:27:55 +0200 Subject: [PATCH 02/29] Reconfigure the actual system GLFW native build directory --- scripts/build_release_linux.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/build_release_linux.sh b/scripts/build_release_linux.sh index 73ecb28..738821d 100755 --- a/scripts/build_release_linux.sh +++ b/scripts/build_release_linux.sh @@ -9,10 +9,10 @@ v=${V_BIN:-v} python3 "$imgui/scripts/prepare-accesskit.py" "$v" run "$imgui/build_vimgui.vsh" --linkage shared --glfw system accesskit="$imgui/.dependencies/accesskit/accesskit-c-0.23.1" -cmake -S "$imgui" -B "$imgui/build/shared-system" \ - -DVIMGUI_APPLICATION_UI=ON "-DACCESSKIT_DIR=$accesskit" \ +cmake -S "$imgui" -B "$imgui/build/shared-system-3.3" \ + -DVIMGUI_APPLICATION_UI=ON "-DVIMGUI_OUTPUT_DIR=$imgui/lib" "-DACCESSKIT_DIR=$accesskit" \ "-DVIMGUI_ACCESSKIT_STATIC_LIBRARY=$accesskit/target/release/libaccesskit.a" -cmake --build "$imgui/build/shared-system" --parallel 4 +cmake --build "$imgui/build/shared-system-3.3" --parallel 4 binaries="$root/build/release-binaries-$variant" mkdir -p "$binaries" for example in glfw_vulkan widget_gallery implot_dashboard; do From fe76ac930bd5cad0a6953e907d25ba692ea93e86 Mon Sep 17 00:00:00 2001 From: antono2 Date: Sat, 3 Oct 2026 05:28:12 +0200 Subject: [PATCH 03/29] Clamp archival license timestamps to the ZIP format range --- scripts/package_linux.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/package_linux.py b/scripts/package_linux.py index 0173ded..e71c3a4 100755 --- a/scripts/package_linux.py +++ b/scripts/package_linux.py @@ -104,7 +104,7 @@ def main(): if source.parent != lib: raise RuntimeError(f'{path.name} depends on an unbundled library: {name} => {source}') archive = package.with_suffix('.zip') - with zipfile.ZipFile(archive, 'w', zipfile.ZIP_DEFLATED, compresslevel=9) as out: + with zipfile.ZipFile(archive, 'w', zipfile.ZIP_DEFLATED, compresslevel=9, strict_timestamps=False) as out: for path in sorted(package.rglob('*')): if path.is_file(): out.write(path, path.relative_to(package.parent)) From 2d689d6f128d979bc9f58f9c6eed2039aed9194b Mon Sep 17 00:00:00 2001 From: antono2 Date: Sat, 3 Oct 2026 06:29:34 +0200 Subject: [PATCH 04/29] Replace Python release tooling with shell PowerShell and V --- .github/workflows/release-packages.yml | 25 +-- IMGUI_REVISION | 2 +- docs/releasing.md | 2 +- examples/android_touch/README.md | 4 +- packaging/PROGGY-LICENSE.txt | 21 +++ packaging/PROGGYFOREVER-LICENSE.txt | 22 +++ scripts/build_release_linux.sh | 7 +- scripts/build_release_windows.ps1 | 4 +- scripts/collect_licenses.ps1 | 21 +++ scripts/collect_licenses.py | 43 ----- scripts/collect_licenses.sh | 37 ++++ scripts/package_linux.py | 115 ------------ scripts/package_linux.sh | 70 ++++++++ scripts/package_windows.ps1 | 5 +- scripts/sign_android_release.py | 70 -------- scripts/sign_android_release.v | 163 ++++++++++++++++++ .../accessibility/AccessibilitySmoke.java | 3 +- tests/desktop/accessibility.cpp | 56 ++++++ tests/desktop/accessibility.ps1 | 60 +++++++ tests/desktop/accessibility.py | 52 ------ 20 files changed, 474 insertions(+), 308 deletions(-) create mode 100644 packaging/PROGGY-LICENSE.txt create mode 100644 packaging/PROGGYFOREVER-LICENSE.txt create mode 100644 scripts/collect_licenses.ps1 delete mode 100644 scripts/collect_licenses.py create mode 100755 scripts/collect_licenses.sh delete mode 100755 scripts/package_linux.py create mode 100755 scripts/package_linux.sh delete mode 100644 scripts/sign_android_release.py create mode 100644 scripts/sign_android_release.v create mode 100644 tests/desktop/accessibility.cpp create mode 100644 tests/desktop/accessibility.ps1 delete mode 100644 tests/desktop/accessibility.py diff --git a/.github/workflows/release-packages.yml b/.github/workflows/release-packages.yml index de1a3f4..80b5993 100644 --- a/.github/workflows/release-packages.yml +++ b/.github/workflows/release-packages.yml @@ -14,9 +14,9 @@ jobs: variant: [docking, standard] include: - variant: docking - imgui-ref: c093593eeb5685ab4c40b308b9773b51fec37ffe + imgui-ref: 161bd5fa99d35fec1192b62cf22348ed785c3277 - variant: standard - imgui-ref: 253cb56d86ab6bb79283980d57ea42e64f091734 + imgui-ref: 8067a7bb52a24e28ca4fbb7f35ff486d224ce91f runs-on: ${{ matrix.os }} timeout-minutes: 45 env: @@ -42,7 +42,7 @@ jobs: path: build/modules/antono2/glfw - name: Install Linux build and rendering tools if: runner.os == 'Linux' - run: sudo apt-get update && sudo apt-get install -y build-essential cmake libglfw3-dev libvulkan-dev libvulkan-volk-dev libdbus-1-dev pkg-config mesa-vulkan-drivers vulkan-validationlayers xvfb patchelf python3-gi python3-dbus gir1.2-atspi-2.0 xdotool + run: sudo apt-get update && sudo apt-get install -y build-essential cmake libglfw3-dev libvulkan-dev libvulkan-volk-dev libdbus-1-dev libatk1.0-dev libatk-bridge2.0-dev libatspi2.0-dev libglib2.0-dev at-spi2-core dbus-x11 pkg-config mesa-vulkan-drivers vulkan-validationlayers xvfb patchelf zip xdotool - name: Install Windows Vulkan build tools if: runner.os == 'Windows' shell: bash @@ -65,11 +65,19 @@ jobs: run: bash scripts/build_release_linux.sh '${{ matrix.variant }}' - name: Exercise bundled gallery through native AT-SPI if: runner.os == 'Linux' - run: dbus-run-session -- xvfb-run -a /usr/bin/python3 tests/desktop/accessibility.py "build/v-imgui-examples-linux-x64-${{ matrix.variant }}/widget_gallery" "build/release-atspi-${{ matrix.variant }}.log" + run: | + c++ -std=c++17 tests/desktop/accessibility.cpp $(pkg-config --cflags --libs atspi-2 gio-2.0) -o build/accessibility-check + GSETTINGS_BACKEND=memory dbus-run-session -- xvfb-run -a build/accessibility-check "build/v-imgui-examples-linux-x64-${{ matrix.variant }}/widget_gallery" "build/release-atspi-${{ matrix.variant }}.log" - name: Build and audit Windows release DLLs if: runner.os == 'Windows' shell: pwsh run: scripts/build_release_windows.ps1 -Variant '${{ matrix.variant }}' + - name: Exercise native Windows UI Automation + if: runner.os == 'Windows' + shell: pwsh + run: | + powershell.exe -NoProfile -ExecutionPolicy Bypass -File tests/desktop/accessibility.ps1 -Executable "build/v-imgui-examples-windows-x64-${{ matrix.variant }}/widget_gallery.exe" + if ($LASTEXITCODE -ne 0) { throw 'Native UI Automation check failed' } - name: Upload portable desktop package uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 with: @@ -110,14 +118,7 @@ jobs: - name: Build accessible touch app run: bash scripts/build_android.sh --build-only - name: Collect Android notices - run: | - case "$ANDROID_ABI" in - armeabi-v7a) target=armv7-linux-androideabi ;; - arm64-v8a) target=aarch64-linux-android ;; - x86_64) target=x86_64-linux-android ;; - esac - python3 scripts/collect_licenses.py build/modules/antono2/imgui "build/android-touch-$ANDROID_ABI/licenses" --target "$target" - cp packaging/ROBOTO-LICENSE.txt "build/android-touch-$ANDROID_ABI/licenses/roboto-license.txt" + run: bash scripts/collect_licenses.sh build/modules/antono2/imgui "build/android-touch-$ANDROID_ABI/licenses" android - name: Upload unsigned APK and notices for release signing uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 with: diff --git a/IMGUI_REVISION b/IMGUI_REVISION index 8933bdc..793309e 100644 --- a/IMGUI_REVISION +++ b/IMGUI_REVISION @@ -1 +1 @@ -c093593eeb5685ab4c40b308b9773b51fec37ffe +161bd5fa99d35fec1192b62cf22348ed785c3277 diff --git a/docs/releasing.md b/docs/releasing.md index d9f84c4..15e9359 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -11,7 +11,7 @@ Desktop ZIPs are ready to promote after reviewing the checks. Android APKs are signed outside CI with the persistent private release key: ```sh -python3 scripts/sign_android_release.py \ +v run scripts/sign_android_release.v \ --input /path/to/android-release-input-armeabi-v7a \ --input /path/to/android-release-input-arm64-v8a \ --input /path/to/android-release-input-x86_64 \ diff --git a/examples/android_touch/README.md b/examples/android_touch/README.md index d53b93c..fce26fb 100644 --- a/examples/android_touch/README.md +++ b/examples/android_touch/README.md @@ -43,8 +43,8 @@ checks do not establish correct behavior on a real keyboard or device. Release APKs use the application/accessibility layer in this same V UI. Set `VIMGUI_ANDROID_APPLICATION_UI=1` before running the build script to enable labelled native accessibility controls, high contrast and the touch-sized theme. -This build also needs Rust and the selected Android Rust target; the upstream -script prepares the pinned AccessKit sources and builds its native adapter. +This build uses the Android SDK/NDK and Java toolchain; the upstream +script builds the native C++ accessibility bridge and Java node provider. The host retains ownership of the input bridge, lifecycle and safe area. End users only download and install an APK; follow diff --git a/packaging/PROGGY-LICENSE.txt b/packaging/PROGGY-LICENSE.txt new file mode 100644 index 0000000..1dfb54d --- /dev/null +++ b/packaging/PROGGY-LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2004, 2005 Tristan Grimmer + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS 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 SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packaging/PROGGYFOREVER-LICENSE.txt b/packaging/PROGGYFOREVER-LICENSE.txt new file mode 100644 index 0000000..01ee2b5 --- /dev/null +++ b/packaging/PROGGYFOREVER-LICENSE.txt @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2026 Disco Hello +Copyright (c) 2019,2023 Tristan Grimmer + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS 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 SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/scripts/build_release_linux.sh b/scripts/build_release_linux.sh index 738821d..a6ff1ce 100755 --- a/scripts/build_release_linux.sh +++ b/scripts/build_release_linux.sh @@ -6,12 +6,9 @@ variant=${1:?Usage: build_release_linux.sh docking|standard} export VMODULES=${VMODULES:-$root/build/modules} export VULKAN_SDK=/usr GLFW_INCLUDE=/usr/include GLFW_LIB=/usr/lib/x86_64-linux-gnu v=${V_BIN:-v} -python3 "$imgui/scripts/prepare-accesskit.py" "$v" run "$imgui/build_vimgui.vsh" --linkage shared --glfw system -accesskit="$imgui/.dependencies/accesskit/accesskit-c-0.23.1" cmake -S "$imgui" -B "$imgui/build/shared-system-3.3" \ - -DVIMGUI_APPLICATION_UI=ON "-DVIMGUI_OUTPUT_DIR=$imgui/lib" "-DACCESSKIT_DIR=$accesskit" \ - "-DVIMGUI_ACCESSKIT_STATIC_LIBRARY=$accesskit/target/release/libaccesskit.a" + -DVIMGUI_APPLICATION_UI=ON "-DVIMGUI_OUTPUT_DIR=$imgui/lib" cmake --build "$imgui/build/shared-system-3.3" --parallel 4 binaries="$root/build/release-binaries-$variant" mkdir -p "$binaries" @@ -20,6 +17,6 @@ for example in glfw_vulkan widget_gallery implot_dashboard; do if [[ $example != glfw_vulkan ]]; then source="$root/examples/$example"; fi "$v" -d release_accessibility -d appui_embedded -no-memory-limit -cc gcc -o "$binaries/$example" "$source" done -python3 "$root/scripts/package_linux.py" "$binaries" "$imgui" "$root/build/v-imgui-examples-linux-x64-$variant" --variant "$variant" +bash "$root/scripts/package_linux.sh" "$binaries" "$imgui" "$root/build/v-imgui-examples-linux-x64-$variant" "$variant" # Check the extracted bundle from outside its original build directory. bash "$root/scripts/smoke_desktop.sh" "$root/build/v-imgui-examples-linux-x64-$variant" diff --git a/scripts/build_release_windows.ps1 b/scripts/build_release_windows.ps1 index 3df68ec..f61fc4d 100644 --- a/scripts/build_release_windows.ps1 +++ b/scripts/build_release_windows.ps1 @@ -9,13 +9,11 @@ Import-Module (Join-Path $InstallPath 'Common7\Tools\Microsoft.VisualStudio.DevS Enter-VsDevShell -VsInstallPath $InstallPath -SkipAutomaticLocation -DevCmdArguments '-arch=x64 -host_arch=x64' $ImguiRoot = Join-Path $env:GITHUB_WORKSPACE 'build\modules\antono2\imgui' -python (Join-Path $ImguiRoot 'scripts/prepare-accesskit.py') --prebuilt -if ($LASTEXITCODE -ne 0) { throw 'Could not prepare AccessKit' } v run (Join-Path $ImguiRoot 'build_vimgui.vsh') --linkage shared --glfw bundled --glfw-version 3.4 if ($LASTEXITCODE -ne 0) { throw 'Could not build the native ImGui library' } $NativeBuild = Join-Path $ImguiRoot 'build\shared-bundled-3.4' -cmake -S $ImguiRoot -B $NativeBuild -DVIMGUI_APPLICATION_UI=ON "-DACCESSKIT_DIR=$ImguiRoot/.dependencies/accesskit/accesskit-c-0.23.1" +cmake -S $ImguiRoot -B $NativeBuild -DVIMGUI_APPLICATION_UI=ON if ($LASTEXITCODE -ne 0) { throw 'Accessible native configure failed' } cmake --build $NativeBuild --config Release --parallel 4 if ($LASTEXITCODE -ne 0) { throw 'Accessible native build failed' } diff --git a/scripts/collect_licenses.ps1 b/scripts/collect_licenses.ps1 new file mode 100644 index 0000000..40a5934 --- /dev/null +++ b/scripts/collect_licenses.ps1 @@ -0,0 +1,21 @@ +param([Parameter(Mandatory)] [string] $Imgui, [Parameter(Mandatory)] [string] $Output) +$ErrorActionPreference = 'Stop' +$Root = Split-Path -Parent $PSScriptRoot +New-Item -ItemType Directory -Force $Output | Out-Null +$Files = @{ + 'examples.txt' = "$Root/LICENSE" + 'imgui-v.txt' = "$Imgui/LICENSE" + 'imgui.txt' = "$Imgui/cimgui/imgui/LICENSE.txt" + 'cimgui.txt' = "$Imgui/cimgui/LICENSE" + 'implot.txt' = "$Imgui/cimplot/implot/LICENSE" + 'cimplot.txt' = "$Imgui/cimplot/LICENSE" + 'proggy.txt' = "$Root/packaging/PROGGY-LICENSE.txt" + 'proggyforever.txt' = "$Root/packaging/PROGGYFOREVER-LICENSE.txt" +} +foreach ($File in $Files.GetEnumerator()) { Copy-Item $File.Value (Join-Path $Output $File.Key) } +$VRoot = Split-Path -Parent (Get-Command v).Source +Copy-Item "$VRoot/LICENSE" (Join-Path $Output 'v.txt') +$Source = Get-Content -Raw "$VRoot/thirdparty/libgc/gc.c" +$Notices = [regex]::Matches($Source, '(?s)/\*.*?\*/') | ForEach-Object { $_.Value } +$Notices | Set-Content -Encoding utf8 (Join-Path $Output 'boehm-gc-notices.txt') +Copy-Item "$VRoot/thirdparty/libatomic_ops/LICENSE" (Join-Path $Output 'libatomic-ops.txt') diff --git a/scripts/collect_licenses.py b/scripts/collect_licenses.py deleted file mode 100644 index e0d6ffc..0000000 --- a/scripts/collect_licenses.py +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env python3 -"""Collect notices for native sources and the pinned AccessKit Rust graph.""" -import argparse -import json -from pathlib import Path -import shutil -import subprocess - -parser = argparse.ArgumentParser(description=__doc__) -parser.add_argument('imgui', type=Path) -parser.add_argument('output', type=Path) -parser.add_argument('--target', required=True) -args = parser.parse_args() -root = Path(__file__).resolve().parent.parent -args.output.mkdir(parents=True, exist_ok=True) -for label, source in { - 'examples': root / 'LICENSE', 'imgui-v': args.imgui / 'LICENSE', - 'imgui': args.imgui / 'cimgui/imgui/LICENSE.txt', - 'cimgui': args.imgui / 'cimgui/LICENSE', - 'implot': args.imgui / 'cimplot/implot/LICENSE', - 'cimplot': args.imgui / 'cimplot/LICENSE', -}.items(): - shutil.copy2(source, args.output / f'{label}.txt') -accesskit = args.imgui / '.dependencies/accesskit/accesskit-c-0.23.1' -metadata = json.loads(subprocess.check_output([ - 'cargo', 'metadata', '--locked', '--format-version', '1', '--filter-platform', args.target, - '--manifest-path', str(accesskit / 'Cargo.toml')], text=True, encoding='utf-8')) -notices = [] -for package in metadata['packages']: - folder = Path(package['manifest_path']).parent - label = f"{package['name']}-{package['version']}" - destination = args.output / 'rust' / label - destination.mkdir(parents=True, exist_ok=True) - notices.append(f"{label}: {package.get('license') or 'See included license file'}") - (destination / 'SOURCE.txt').write_text( - f"{label}\nLicense: {package.get('license')}\nSource: {package.get('repository') or package.get('source') or 'AccessKit release source'}\n") - for file in folder.iterdir(): - if file.is_file() and any(word in file.name.upper() for word in ('LICENSE', 'COPYING', 'NOTICE')): - shutil.copy2(file, destination / file.name) - custom = package.get('license_file') - if custom: - shutil.copy2(folder / custom, destination / Path(custom).name) -(args.output / 'RUST-DEPENDENCIES.txt').write_text('\n'.join(sorted(notices)) + '\n') diff --git a/scripts/collect_licenses.sh b/scripts/collect_licenses.sh new file mode 100755 index 0000000..d1fdb38 --- /dev/null +++ b/scripts/collect_licenses.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +# Copy redistribution notices for the components actually used by the builds. +set -euo pipefail +root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) +imgui=${1:?Usage: collect_licenses.sh imgui-dir output-dir [android]} +output=${2:?Missing output directory} +mkdir -p "$output" +cp "$root/LICENSE" "$output/examples.txt" +cp "$imgui/LICENSE" "$output/imgui-v.txt" +cp "$imgui/cimgui/imgui/LICENSE.txt" "$output/imgui.txt" +cp "$imgui/cimgui/LICENSE" "$output/cimgui.txt" +cp "$imgui/cimplot/implot/LICENSE" "$output/implot.txt" +cp "$imgui/cimplot/LICENSE" "$output/cimplot.txt" +cp "$root/packaging/PROGGY-LICENSE.txt" "$root/packaging/PROGGYFOREVER-LICENSE.txt" "$output/" +if [[ ${3:-} == android ]]; then + cp "$root/packaging/ROBOTO-LICENSE.txt" "$output/" + freetype="$imgui/third_party/freetype" + cp "$freetype/LICENSE.TXT" "$output/freetype-license.txt" + cp "$freetype/docs/FTL.TXT" "$output/freetype-FTL.txt" + cp "$freetype/subprojects/dlg/LICENSE" "$output/freetype-dlg.txt" + sed -n '1,/\*\//p' "$freetype/src/gzip/zlib.h" > "$output/freetype-zlib.txt" + cat > "$output/freetype-attribution.txt" <<'NOTICE' +This app includes FreeType 2.13.3. Portions of this software are copyright +© 2024 The FreeType Project (www.freetype.org). All rights reserved. +FreeType is redistributed under the FreeType License (FTL). +NOTICE +fi +v_binary=${V_BIN:-$(command -v v)} +v_root=$(cd -- "$(dirname -- "$v_binary")" && pwd) +cp "$v_root/LICENSE" "$output/v.txt" +if [[ ${3:-} != android ]]; then + # The default desktop collector is statically linked. Retain every source + # comment, including component copyright and permission notices. + awk 'index($0,"/*") {block=1} block {print} index($0,"*/") {block=0}' \ + "$v_root/thirdparty/libgc/gc.c" > "$output/boehm-gc-notices.txt" + cp "$v_root/thirdparty/libatomic_ops/LICENSE" "$output/libatomic-ops.txt" +fi diff --git a/scripts/package_linux.py b/scripts/package_linux.py deleted file mode 100755 index e71c3a4..0000000 --- a/scripts/package_linux.py +++ /dev/null @@ -1,115 +0,0 @@ -#!/usr/bin/env python3 -"""Stage trusted Linux examples and their non-glibc runtime dependencies.""" -import argparse -import hashlib -import os -from pathlib import Path -import re -import shutil -import subprocess -import zipfile - -BASE = re.compile(r'^(lib(c|m|pthread|dl|rt|resolv|util|mvec)\.so\.|ld-linux|linux-vdso)') -EXAMPLES = ('glfw_vulkan', 'widget_gallery', 'implot_dashboard') - - -def run(*args): - return subprocess.check_output(args, text=True, env={**os.environ, 'LD_LIBRARY_PATH': ''}) - - -def dependencies(path): - result = run('ldd', str(path)) - if 'not found' in result: - raise RuntimeError(f'Unresolved dependency for {path}:\n{result}') - for line in result.splitlines(): - match = re.match(r'\s*(\S+) => (/\S+) ', line) - if match and not BASE.match(match[1]): - yield match[1], Path(match[2]) - - -def main(): - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument('binaries', type=Path) - parser.add_argument('imgui', type=Path) - parser.add_argument('output', type=Path) - parser.add_argument('--variant', choices=['docking', 'standard'], required=True) - args = parser.parse_args() - root = Path(__file__).resolve().parent.parent - package = args.output.resolve() - package.mkdir(parents=True, exist_ok=False) - lib = package / 'lib' - notices = package / 'licenses' - lib.mkdir() - notices.mkdir() - queue = [] - for name in EXAMPLES: - source = args.binaries / name - shutil.copy2(source, package / name) - queue.extend(dependencies(source)) - # GLFW loads these with dlopen(), so they do not all appear in ldd. - for name in ('libvulkan.so.1', 'libXrandr.so.2', 'libXinerama.so.1', - 'libXcursor.so.1', 'libXi.so.6', 'libXxf86vm.so.1'): - source = Path('/usr/lib/x86_64-linux-gnu') / name - if not source.exists(): - raise RuntimeError(f'Missing runtime library: {source}') - queue.append((name, source)) - copied = {} - owners = set() - while queue: - name, source = queue.pop() - digest = hashlib.sha256(source.read_bytes()).hexdigest() - if name in copied: - if copied[name] != digest: - raise RuntimeError(f'Conflicting runtime library: {name}') - continue - copied[name] = digest - shutil.copy2(source, lib / name) - queue.extend(dependencies(source)) - # Debian/Ubuntu copyright files include upstream license notices. - candidates = {str(source), str(source.resolve())} - for candidate in list(candidates): - if candidate.startswith('/usr/lib/'): - candidates.add(candidate.removeprefix('/usr')) - elif candidate.startswith('/lib/'): - candidates.add('/usr' + candidate) - for candidate in candidates: - result = subprocess.run(['dpkg-query', '-S', candidate], text=True, capture_output=True) - if result.returncode == 0: - for line in result.stdout.splitlines(): - owner = line.split(': ', 1)[0].split(':')[0] - copyright_file = Path('/usr/share/doc') / owner / 'copyright' - if copyright_file.exists(): - owners.add(owner) - for owner in owners: - shutil.copy2(Path('/usr/share/doc') / owner / 'copyright', notices / f'{owner}.txt') - for label, source in { - 'examples': root / 'LICENSE', 'imgui-v': args.imgui / 'LICENSE', - 'imgui': args.imgui / 'cimgui/imgui/LICENSE.txt', - 'cimgui': args.imgui / 'cimgui/LICENSE', - 'implot': args.imgui / 'cimplot/implot/LICENSE', - 'cimplot': args.imgui / 'cimplot/LICENSE', - }.items(): - shutil.copy2(source, notices / f'{label}.txt') - subprocess.run(['python3', str(root / 'scripts/collect_licenses.py'), str(args.imgui), str(notices), - '--target', 'x86_64-unknown-linux-gnu'], check=True) - shutil.copy2(root / 'packaging/run.sh', package / 'run.sh') - (package / 'run.sh').chmod(0o755) - shutil.copy2(root / 'packaging/README-linux.txt', package / 'README.txt') - (package / 'VARIANT.txt').write_text(args.variant + '\n') - (package / 'RUNTIME-LIBRARIES.txt').write_text('\n'.join(sorted(copied)) + '\n') - for path in [*(package / name for name in EXAMPLES), *lib.iterdir()]: - subprocess.run(['patchelf', '--set-rpath', '$ORIGIN/lib' if path.parent == package else '$ORIGIN', str(path)], check=True) - for path in [*(package / name for name in EXAMPLES), *lib.iterdir()]: - for name, source in dependencies(path): - if source.parent != lib: - raise RuntimeError(f'{path.name} depends on an unbundled library: {name} => {source}') - archive = package.with_suffix('.zip') - with zipfile.ZipFile(archive, 'w', zipfile.ZIP_DEFLATED, compresslevel=9, strict_timestamps=False) as out: - for path in sorted(package.rglob('*')): - if path.is_file(): - out.write(path, path.relative_to(package.parent)) - print(archive) - - -if __name__ == '__main__': - main() diff --git a/scripts/package_linux.sh b/scripts/package_linux.sh new file mode 100755 index 0000000..c6736e5 --- /dev/null +++ b/scripts/package_linux.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +set -euo pipefail +root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) +binaries=${1:?Usage: package_linux.sh binary-dir imgui-dir output-dir docking|standard} +imgui=${2:?Missing ImGui directory} +output=${3:?Missing output directory} +variant=${4:?Missing variant} +[[ $variant == docking || $variant == standard ]] +[[ ! -e $output ]] +mkdir -p "$output/lib" "$output/licenses" +output=$(cd "$output" && pwd) +queue=() +declare -A digests +base_library() { [[ $1 =~ ^(lib(c|m|pthread|dl|rt|resolv|util|mvec)\.so\.|ld-linux|linux-vdso) ]]; } +dependencies() { + local file=$1 line name destination + local listing + listing=$(LD_LIBRARY_PATH= ldd "$file") + if [[ $listing == *'not found'* ]]; then printf '%s\n' "$listing" >&2; return 1; fi + while IFS= read -r line; do + if [[ $line =~ ^[[:space:]]*([^[:space:]]+)[[:space:]]+\=\>[[:space:]]+(/[^[:space:]]+)[[:space:]] ]]; then + name=${BASH_REMATCH[1]}; destination=${BASH_REMATCH[2]} + if ! base_library "$name"; then queue+=("$name" "$destination"); fi + fi + done <<< "$listing" +} +for example in glfw_vulkan widget_gallery implot_dashboard; do + cp "$binaries/$example" "$output/$example" + dependencies "$binaries/$example" +done +# GLFW uses dlopen for extension libraries and Vulkan presentation. +for name in libvulkan.so.1 libXrandr.so.2 libXinerama.so.1 libXcursor.so.1 libXi.so.6 libXxf86vm.so.1; do + queue+=("$name" "/usr/lib/x86_64-linux-gnu/$name") +done +index=0 +while (( index < ${#queue[@]} )); do + name=${queue[index]}; file=${queue[index+1]}; index=$((index+2)) + digest=$(sha256sum "$file"); digest=${digest%% *} + if [[ -v digests[$name] ]]; then [[ ${digests[$name]} == "$digest" ]]; continue; fi + digests[$name]=$digest + cp -L "$file" "$output/lib/$name" + dependencies "$file" + for candidate in "$file" "$(readlink -f "$file")" "${file#/usr}"; do + while IFS= read -r owner; do + owner=${owner%%: *}; owner=${owner%%:*} + if [[ -f /usr/share/doc/$owner/copyright ]]; then + cp "/usr/share/doc/$owner/copyright" "$output/licenses/$owner.txt" + fi + done < <(dpkg-query -S "$candidate" 2>/dev/null || true) + done +done +bash "$root/scripts/collect_licenses.sh" "$imgui" "$output/licenses" +cp "$root/packaging/run.sh" "$output/run.sh" +chmod +x "$output/run.sh" +cp "$root/packaging/README-linux.txt" "$output/README.txt" +printf '%s\n' "$variant" > "$output/VARIANT.txt" +printf '%s\n' "${!digests[@]}" | sort > "$output/RUNTIME-LIBRARIES.txt" +for example in glfw_vulkan widget_gallery implot_dashboard; do + patchelf --set-rpath '$ORIGIN/lib' "$output/$example" +done +for file in "$output"/lib/*; do patchelf --set-rpath '$ORIGIN' "$file"; done +queue=() +for file in "$output"/glfw_vulkan "$output"/widget_gallery "$output"/implot_dashboard "$output"/lib/*; do dependencies "$file"; done +for ((index=0;index<${#queue[@]};index+=2)); do + [[ ${queue[index+1]} == "$output/lib/"* ]] || { printf 'Unbundled library: %s\n' "${queue[index+1]}" >&2; exit 1; } +done +# Store timestamps in the range supported by ZIP, including archive license files. +find "$output" -type f ! -newermt 1980-01-01 -exec touch -t 198001010000 {} + +(cd "$(dirname "$output")"; zip -q -r -9 "$(basename "$output").zip" "$(basename "$output")") +printf '%s.zip\n' "$output" diff --git a/scripts/package_windows.ps1 b/scripts/package_windows.ps1 index 590b056..7ddb8ae 100644 --- a/scripts/package_windows.ps1 +++ b/scripts/package_windows.ps1 @@ -18,8 +18,7 @@ $Crt = Get-ChildItem (Join-Path $env:VCToolsRedistDir 'x64') -Directory -Filter if (-not $Crt) { throw 'The redistributable x64 C runtime was not found' } Get-ChildItem $Crt.FullName -Filter '*.dll' | Copy-Item -Destination $Output Copy-Item (Join-Path $env:VULKAN_SDK 'Bin/vulkan-1.dll') $Output -python (Join-Path $PSScriptRoot 'collect_licenses.py') $Imgui $Licenses --target x86_64-pc-windows-msvc -if ($LASTEXITCODE -ne 0) { throw 'License collection failed' } +& (Join-Path $PSScriptRoot 'collect_licenses.ps1') -Imgui $Imgui -Output $Licenses $GlfwLicense = Get-ChildItem (Join-Path $Imgui 'build/shared-bundled-3.4') -Recurse -Filter 'LICENSE.md' | Where-Object { $_.FullName -match 'glfw-src' } | Select-Object -First 1 if (-not $GlfwLicense) { throw 'GLFW license not found' } @@ -43,7 +42,7 @@ foreach ($File in Get-ChildItem $Output -File | Where-Object { $_.Extension -in 'oleaut32.dll','comdlg32.dll','shlwapi.dll','setupapi.dll','cfgmgr32.dll','ntdll.dll','ucrtbase.dll', 'ws2_32.dll','bcrypt.dll','crypt32.dll','secur32.dll','rpcrt4.dll','dwmapi.dll','version.dll', 'userenv.dll','imm32.dll','winmm.dll','msvcrt.dll','powrprof.dll','uxtheme.dll', - 'hid.dll','winspool.drv','uiautomationcore.dll','propsys.dll','netapi32.dll','mswsock.dll', + 'hid.dll','winspool.drv','uiautomationcore.dll','comctl32.dll','propsys.dll','netapi32.dll','mswsock.dll', 'normaliz.dll','d3d11.dll','dxgi.dll','opengl32.dll') if ($Name -notin $OsDlls -and $Name -notmatch '^(api|ext)-ms-') { throw "Unbundled runtime dependency: $($File.Name) -> $Name" diff --git a/scripts/sign_android_release.py b/scripts/sign_android_release.py deleted file mode 100644 index f53e7e4..0000000 --- a/scripts/sign_android_release.py +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env python3 -"""Combine CI ABI inputs and sign installable APKs with a private release key.""" -import argparse -import hashlib -from pathlib import Path -import re -import subprocess -import tempfile -import zipfile - -parser = argparse.ArgumentParser(description=__doc__) -parser.add_argument('--input', type=Path, action='append', required=True, - help='Extracted CI artifact directory with unsigned APK and licenses') -parser.add_argument('--output', type=Path, required=True) -parser.add_argument('--build-tools', type=Path, required=True) -parser.add_argument('--keystore', type=Path, required=True) -parser.add_argument('--password-file', type=Path, required=True) -parser.add_argument('--alias', default='v-imgui-release') -args = parser.parse_args() -args.output.mkdir(parents=True, exist_ok=True) -shared = None -libraries = {} -notices = {} -for folder in args.input: - apk = folder / 'vimgui-demo-unsigned.apk' - with zipfile.ZipFile(apk) as archive: - common = {} - for name in archive.namelist(): - if name.endswith('/') or name.startswith('META-INF/'): continue - data = archive.read(name) - if name.startswith('lib/'): - if not re.fullmatch(r'lib/(armeabi-v7a|arm64-v8a|x86_64)/[^/]+\.so', name): - raise ValueError(f'Unexpected native library path: {name}') - if name in libraries: raise ValueError(f'Duplicate native input: {name}') - libraries[name] = data - else: - common[name] = data - if shared is None: shared = common - elif shared != common: raise ValueError('ABI inputs disagree on the manifest, UI classes or assets') - for file in (folder / 'licenses').rglob('*'): - if not file.is_file(): continue - name = 'assets/licenses/' + file.relative_to(folder / 'licenses').as_posix() - if name in notices and notices[name] != file.read_bytes(): - raise ValueError(f'License inputs disagree: {name}') - notices[name] = file.read_bytes() -abis = {name.split('/')[1] for name in libraries} -if abis != {'armeabi-v7a', 'arm64-v8a', 'x86_64'}: - raise ValueError(f'Expected all three supported ABIs, got {abis}') -for abi in abis: - expected = {f'lib/{abi}/{name}' for name in ('libvimgui.so', 'libvimgui_android_demo.so', 'libvimgui_android_ui.so')} - if {name for name in libraries if name.split('/')[1] == abi} != expected: - raise ValueError(f'Incomplete ABI: {abi}') -with tempfile.TemporaryDirectory() as temporary: - temporary = Path(temporary) - for variant in ['universal', *sorted(abis)]: - unsigned = temporary / 'unsigned.apk' - aligned = temporary / 'aligned.apk' - output = args.output / f'v-imgui-touch-{variant}.apk' - with zipfile.ZipFile(unsigned, 'w', zipfile.ZIP_DEFLATED, compresslevel=9) as archive: - for name, data in sorted({**shared, **notices, **libraries}.items()): - if name.startswith('lib/') and variant != 'universal' and name.split('/')[1] != variant: continue - entry = zipfile.ZipInfo(name, date_time=(2026, 1, 1, 0, 0, 0)) - entry.compress_type = zipfile.ZIP_DEFLATED - archive.writestr(entry, data) - subprocess.run([str(args.build_tools / 'zipalign'), '-f', '-p', '4', str(unsigned), str(aligned)], check=True) - subprocess.run([str(args.build_tools / 'apksigner'), 'sign', '--ks', str(args.keystore), - '--ks-key-alias', args.alias, '--ks-pass', 'file:' + str(args.password_file), - '--key-pass', 'file:' + str(args.password_file), '--out', str(output), str(aligned)], check=True) - subprocess.run([str(args.build_tools / 'apksigner'), 'verify', '--verbose', str(output)], check=True) - print(f'{hashlib.sha256(output.read_bytes()).hexdigest()} {output.name}') diff --git a/scripts/sign_android_release.v b/scripts/sign_android_release.v new file mode 100644 index 0000000..a4ae095 --- /dev/null +++ b/scripts/sign_android_release.v @@ -0,0 +1,163 @@ +// Combine trusted unsigned CI APKs and sign with the private release identity. +module main + +import os +import rand +import compress.szip +import crypto.sha256 + +fn command(program string, args []string) ! { + mut process := os.new_process(program) + process.set_args(args) + process.run() + process.wait() + code := process.code + process.close() + if code != 0 { + return error('${program} failed (${code})') + } +} + +fn archive_entries(path string) !map[string][]u8 { + mut archive := szip.open(path, .no_compression, .read_only)! + defer { archive.close() } + mut entries := map[string][]u8{} + for index in 0 .. archive.total()! { + archive.open_entry_by_index(index)! + name := archive.name() + if name.ends_with('/') || name.starts_with('META-INF/') { + archive.close_entry() + continue + } + if name.starts_with('/') || name.split('/').contains('..') { + return error('Invalid archive path: ${name}') + } + mut data := []u8{len: int(archive.size())} + archive.read_entry_buf(data.data, data.len)! + entries[name] = data + archive.close_entry() + } + return entries +} + +fn main() { + run() or { + eprintln(err) + exit(1) + } +} + +fn run() ! { + mut inputs := []string{} + mut options := map[string]string{} + mut index := 1 + for index < os.args.len { + if index + 1 >= os.args.len { + return error('Missing value for ${os.args[index]}') + } + flag := os.args[index] + value := os.args[index + 1] + if flag == '--input' { + inputs << value + } else { + options[flag] = value + } + index += 2 + } + for key in ['--output', '--build-tools', '--keystore', '--password-file'] { + if options[key] == '' { + return error('Missing ${key}') + } + } + if inputs.len != 3 { + return error('Provide the three ABI input directories with --input') + } + abis := ['armeabi-v7a', 'arm64-v8a', 'x86_64'] + mut common_entries := map[string][]u8{} + mut libraries := map[string][]u8{} + mut notices := map[string][]u8{} + for position, folder in inputs { + entries := archive_entries(os.join_path(folder, 'vimgui-demo-unsigned.apk'))! + mut common := map[string][]u8{} + for name, bytes in entries { + if name.starts_with('lib/') { + parts := name.split('/') + if parts.len != 3 || parts[1] !in abis || !parts[2].ends_with('.so') { + return error('Unexpected native library: ${name}') + } + if name in libraries { + return error('Duplicate native input: ${name}') + } + libraries[name] = bytes + } else { + common[name] = bytes + } + } + if position == 0 { + common_entries = common.clone() + } else if common_entries != common { + return error('ABI inputs disagree on the manifest, UI classes or assets') + } + license_dir := os.join_path(folder, 'licenses') + if !os.is_dir(license_dir) { + return error('Missing redistribution notices: ${license_dir}') + } + for file in os.walk_ext(license_dir, '') { + if !os.is_file(file) { + continue + } + name := 'assets/licenses/' + file.trim_string_left(license_dir + os.path_separator).replace('\\', '/') + bytes := os.read_bytes(file)! + if name in notices && notices[name] != bytes { + return error('License inputs disagree: ${name}') + } + notices[name] = bytes + } + } + for abi in abis { + for name in ['libvimgui.so', 'libvimgui_android_demo.so', 'libvimgui_android_ui.so'] { + if 'lib/${abi}/${name}' !in libraries { + return error('Incomplete ABI: ${abi}') + } + } + } + if libraries.len != 9 { + return error('Unexpected extra native libraries') + } + os.mkdir_all(options['--output'])! + temporary := os.join_path(os.temp_dir(), 'v-imgui-sign-' + rand.uuid_v4()) + os.mkdir(temporary, mode: 0o700)! + defer { os.rmdir_all(temporary) or {} } + for variant in ['universal', 'armeabi-v7a', 'arm64-v8a', 'x86_64'] { + unsigned := os.join_path(temporary, 'unsigned.apk') + aligned := os.join_path(temporary, 'aligned.apk') + output := os.join_path(options['--output'], 'v-imgui-touch-${variant}.apk') + mut contents := common_entries.clone() + for name, bytes in notices { + contents[name] = bytes + } + for name, bytes in libraries { + if variant == 'universal' || name.split('/')[1] == variant { + contents[name] = bytes + } + } + mut archive := szip.open(unsigned, .best_compression, .write)! + mut names := contents.keys() + names.sort() + for name in names { + archive.open_entry(name)! + archive.write_entry(contents[name])! + archive.close_entry() + } + archive.close() + command(os.join_path(options['--build-tools'], 'zipalign'), ['-f', '-p', '4', unsigned, + aligned])! + alias := if options['--alias'] != '' { options['--alias'] } else { 'v-imgui-release' } + password := 'file:' + options['--password-file'] + command(os.join_path(options['--build-tools'], 'apksigner'), ['sign', '--ks', + options['--keystore'], '--ks-key-alias', alias, '--ks-pass', password, '--key-pass', + password, '--out', output, aligned])! + command(os.join_path(options['--build-tools'], 'apksigner'), ['verify', '--verbose', output])! + println('${sha256.hexhash(os.read_file(output)!)} ${os.file_name(output)}') + } +} diff --git a/tests/android/accessibility/AccessibilitySmoke.java b/tests/android/accessibility/AccessibilitySmoke.java index abb9f4e..148c94e 100644 --- a/tests/android/accessibility/AccessibilitySmoke.java +++ b/tests/android/accessibility/AccessibilitySmoke.java @@ -128,7 +128,8 @@ private void drag(float x, float from, float to) { SystemClock.sleep(500); Rect before=new Rect();node("Tap counter").getBoundsInScreen(before); Rect instructions=new Rect();node("Copy uses the entire editable field; no selection is needed. Read displays the clipboard below.").getBoundsInScreen(instructions); - if(instructions.height()>0 && instructions.top>150) { + if(!(instructions.height()>0 && instructions.top>150))throw new AssertionError("Static swipe target is not visible"); + { drag(instructions.centerX(),instructions.centerY(),100); await(() -> {Rect after=new Rect();node("Tap counter").getBoundsInScreen(after);return after.top +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +static void settle(){while(g_main_context_pending(nullptr))g_main_context_iteration(nullptr,false);std::this_thread::sleep_for(std::chrono::milliseconds(100));} +static AtspiAccessible *find(const std::string &name){std::deque queue;queue.push_back(atspi_get_desktop(0)); + while(!queue.empty()){auto *object=queue.front();queue.pop_front();if(!object)continue;gchar *label=atspi_accessible_get_name(object,nullptr); + bool match=label && name==label;g_free(label); + if(match){for(auto *remaining:queue)if(remaining)g_object_unref(remaining);return object;} + for(int i=0;imessage);g_error_free(error);return 1;} + for(auto property:{"IsEnabled","ScreenReaderEnabled"}){auto *reply=g_dbus_connection_call_sync(bus,"org.a11y.Bus","/org/a11y/bus","org.freedesktop.DBus.Properties","Set", + g_variant_new("(ssv)","org.a11y.Status",property,g_variant_new_boolean(true)),nullptr,G_DBUS_CALL_FLAGS_NONE,10000,nullptr,&error); + if(!reply){std::fprintf(stderr,"%s\n",error->message);g_error_free(error);g_object_unref(bus);return 1;}g_variant_unref(reply);} + g_object_unref(bus);atspi_init();pid_t child=fork();if(child==0){std::freopen(argv[2],"w",stdout);dup2(fileno(stdout),STDERR_FILENO);execl(argv[1],argv[1],nullptr);_exit(127);} + int result=0; + try { + for(auto name:{"High contrast","Name","Count","Files"}){auto *node=require(name);auto *role=atspi_accessible_get_role_name(node,nullptr);std::printf("%s: %s\n",name,role);g_free(role);g_object_unref(node);} + if(std::system("xdotool search --sync --name 'V ImGui: widget gallery' windowfocus")!=0)throw std::runtime_error("Could not focus gallery"); + click("Count");expect("Count: 1");click("High contrast");has_state("High contrast",ATSPI_STATE_CHECKED); + click("200% text");click("Focus last file");has_state("Photo 0999.jpg",ATSPI_STATE_FOCUSED); + click("Photo 0999.jpg");expect("Selected Photo 0999.jpg");click("Raw ImGui widgets");settle();click("Accessible controls");expect("Files"); + auto *field=require("Name");auto *edit=atspi_accessible_get_editable_text_iface(field); + if(!edit||!atspi_editable_text_set_text_contents(edit,"A📷éZ",nullptr))throw std::runtime_error("Unicode native edit rejected");g_object_unref(edit);g_object_unref(field);settle(); + field=require("Name");auto *text=atspi_accessible_get_text_iface(field);gchar *value=atspi_text_get_text(text,0,-1,nullptr); + if(!value||std::string(value)!="A📷éZ")throw std::runtime_error("Unicode text did not round-trip");g_free(value); + if(!atspi_text_set_selection(text,0,1,2,nullptr))throw std::runtime_error("Native text selection rejected");g_object_unref(text);g_object_unref(field); + std::puts("PASS: native AT-SPI roles, actions, contrast, text size, virtual list focus and selection, views, Unicode editing and selection"); + }catch(const std::exception &error){std::fprintf(stderr,"%s\n",error.what());result=1;} + kill(child,SIGTERM);waitpid(child,nullptr,0);atspi_exit();return result; +} diff --git a/tests/desktop/accessibility.ps1 b/tests/desktop/accessibility.ps1 new file mode 100644 index 0000000..5e885cb --- /dev/null +++ b/tests/desktop/accessibility.ps1 @@ -0,0 +1,60 @@ +param([Parameter(Mandatory)] [string] $Executable) +$ErrorActionPreference = 'Stop' +Add-Type -AssemblyName UIAutomationClient +Add-Type -AssemblyName UIAutomationTypes +$Executable = (Resolve-Path $Executable).Path +$Process = Start-Process -FilePath $Executable -WorkingDirectory (Split-Path $Executable) -PassThru +function Wait-For([scriptblock] $Probe, [string] $Message) { + $Deadline = [DateTime]::UtcNow.AddSeconds(30) + do { + if ($Process.HasExited) { throw "Gallery exited with $($Process.ExitCode): $Message" } + $Value = & $Probe + if ($Value) { return $Value } + Start-Sleep -Milliseconds 100 + } while ([DateTime]::UtcNow -lt $Deadline) + throw $Message +} +function Find-Control([string] $Name) { + $Condition = New-Object System.Windows.Automation.PropertyCondition([System.Windows.Automation.AutomationElement]::NameProperty, $Name) + $Root.FindFirst([System.Windows.Automation.TreeScope]::Descendants, $Condition) +} +function Require-Control([string] $Name) { Wait-For { Find-Control $Name } "Missing UIA control: $Name" } +function Invoke-Control([string] $Name) { + $Node = Require-Control $Name + $Pattern = $Node.GetCurrentPattern([System.Windows.Automation.InvokePattern]::Pattern) + $Pattern.Invoke() +} +try { + $Handle = Wait-For { $Process.Refresh(); if ($Process.MainWindowHandle -ne 0) { $Process.MainWindowHandle } } 'No gallery window' + $Root = [System.Windows.Automation.AutomationElement]::FromHandle($Handle) + foreach ($Name in @('Count', 'Name', 'High contrast', 'Files')) { $Node = Require-Control $Name; Write-Output "$Name : $($Node.Current.ControlType.ProgrammaticName)" } + Invoke-Control 'Count'; $null = Require-Control 'Count: 1' + $Toggle = (Require-Control 'High contrast').GetCurrentPattern([System.Windows.Automation.TogglePattern]::Pattern) + $Toggle.Toggle() + $null = Wait-For { $Toggle.Current.ToggleState -eq [System.Windows.Automation.ToggleState]::On } 'Checkbox state did not update' + Invoke-Control '200% text'; Invoke-Control 'Focus last file' + $Last = Require-Control 'Photo 0999.jpg' + $Last.GetCurrentPattern([System.Windows.Automation.SelectionItemPattern]::Pattern).Select() + $null = Require-Control 'Selected Photo 0999.jpg' + $List = (Require-Control 'Files').GetCurrentPattern([System.Windows.Automation.ScrollPattern]::Pattern) + $List.SetScrollPercent(-1, 50) + $null = Wait-For { [Math]::Abs($List.Current.VerticalScrollPercent - 50) -lt 1 } 'Exact scroll percentage did not apply' + Invoke-Control 'Raw ImGui widgets'; $null = Require-Control 'Accessible controls' + Invoke-Control 'Accessible controls'; $null = Require-Control 'Name' + $Unicode = 'A' + [char]0xd83d + [char]0xdcf7 + 'e' + [char]0x0301 + 'Z' + $Field = Require-Control 'Name' + $Value = $Field.GetCurrentPattern([System.Windows.Automation.ValuePattern]::Pattern) + $Value.SetValue($Unicode) + $null = Wait-For { $Value.Current.Value -eq $Unicode } 'Unicode native edit did not round-trip' + $Text = $Field.GetCurrentPattern([System.Windows.Automation.TextPattern]::Pattern) + $Range = $Text.DocumentRange.Clone() + $Range.MoveEndpointByRange([System.Windows.Automation.TextPatternRangeEndpoint]::End, $Range, [System.Windows.Automation.TextPatternRangeEndpoint]::Start) + $null = $Range.MoveEndpointByUnit([System.Windows.Automation.TextPatternRangeEndpoint]::Start, [System.Windows.Automation.TextUnit]::Character, 1) + $null = $Range.MoveEndpointByUnit([System.Windows.Automation.TextPatternRangeEndpoint]::End, [System.Windows.Automation.TextUnit]::Character, 1) + $Range.Select() + $Camera = [string][char]0xd83d + [char]0xdcf7 + $null = Wait-For { $Selection = $Text.GetSelection(); $Selection.Count -eq 1 -and $Selection[0].GetText(-1) -eq $Camera } 'Unicode selection did not round-trip' + Write-Output 'PASS: native UIA roles, actions, checkbox state, text scaling, virtual selection, exact scrolling, views and Unicode text/selection' +} finally { + if (-not $Process.HasExited) { Stop-Process -Id $Process.Id -Force } +} diff --git a/tests/desktop/accessibility.py b/tests/desktop/accessibility.py deleted file mode 100644 index f7ef1de..0000000 --- a/tests/desktop/accessibility.py +++ /dev/null @@ -1,52 +0,0 @@ -import gi,sys,time,subprocess,os -gi.require_version('Atspi','2.0') -from gi.repository import Atspi,GLib -import dbus -bus=dbus.SessionBus() -properties=dbus.Interface(bus.get_object("org.a11y.Bus","/org/a11y/bus"),"org.freedesktop.DBus.Properties") -properties.Set("org.a11y.Status","IsEnabled",dbus.Boolean(True)) -properties.Set("org.a11y.Status","ScreenReaderEnabled",dbus.Boolean(True)) -process=subprocess.Popen([sys.argv[1]], stdout=open(sys.argv[2],'w'),stderr=subprocess.STDOUT) -def find(name): - q=[Atspi.get_desktop(0)] - while q: - n=q.pop(0) - try: - if n.get_name()==name: return n - q.extend(n.get_child_at_index(i) for i in range(n.get_child_count())) - except Exception: pass - return None -def await_node(name): - end=time.monotonic()+20 - while time.monotonic()0,name - for i in range(a.get_n_actions()): - if a.get_action_name(i) in ('click','press','activate','toggle'): - assert a.do_action(i);return - raise AssertionError('No click action: '+name) -try: - print('Roles:',[(name,await_node(name).get_role_name()) for name in ('High contrast','Name','Count','Files')],flush=True) - subprocess.run(['xdotool','search','--sync','--name','V ImGui: widget gallery','windowfocus'],check=True) - click('Count');await_node('Count: 1') - click('High contrast');time.sleep(.2) - assert await_node('High contrast').get_state_set().contains(Atspi.StateType.CHECKED) - click('200% text');time.sleep(.2) - click('Focus last file');last=await_node('Photo 0999.jpg') - end=time.monotonic()+10 - while not last.get_state_set().contains(Atspi.StateType.FOCUSED) and time.monotonic() Date: Sat, 3 Oct 2026 06:47:04 +0200 Subject: [PATCH 05/29] Share release build packaging and license helpers in V --- .github/workflows/release-packages.yml | 13 +- IMGUI_REVISION | 2 +- docs/releasing.md | 12 +- examples/widget_gallery/README.md | 3 +- scripts/build_release.v | 109 +++++++++++ scripts/build_release_linux.sh | 22 --- scripts/build_release_windows.ps1 | 50 ----- scripts/collect_licenses.ps1 | 21 --- scripts/collect_licenses.sh | 37 ---- scripts/collect_licenses.v | 15 ++ scripts/package_linux.sh | 70 ------- scripts/package_release.v | 15 ++ scripts/package_windows.ps1 | 55 ------ scripts/release_tools/common.v | 41 +++++ scripts/release_tools/licenses.v | 54 ++++++ scripts/release_tools/package.v | 245 +++++++++++++++++++++++++ scripts/release_tools/zip.v | 145 +++++++++++++++ scripts/setup_windows_build.ps1 | 7 + scripts/sign_android_release.v | 48 +++-- 19 files changed, 673 insertions(+), 291 deletions(-) create mode 100644 scripts/build_release.v delete mode 100755 scripts/build_release_linux.sh delete mode 100644 scripts/build_release_windows.ps1 delete mode 100644 scripts/collect_licenses.ps1 delete mode 100755 scripts/collect_licenses.sh create mode 100644 scripts/collect_licenses.v delete mode 100755 scripts/package_linux.sh create mode 100644 scripts/package_release.v delete mode 100644 scripts/package_windows.ps1 create mode 100644 scripts/release_tools/common.v create mode 100644 scripts/release_tools/licenses.v create mode 100644 scripts/release_tools/package.v create mode 100644 scripts/release_tools/zip.v create mode 100644 scripts/setup_windows_build.ps1 diff --git a/.github/workflows/release-packages.yml b/.github/workflows/release-packages.yml index 80b5993..0e8e118 100644 --- a/.github/workflows/release-packages.yml +++ b/.github/workflows/release-packages.yml @@ -14,9 +14,9 @@ jobs: variant: [docking, standard] include: - variant: docking - imgui-ref: 161bd5fa99d35fec1192b62cf22348ed785c3277 + imgui-ref: 93401818d99f80afe6b97c042f215cc4f11dc7de - variant: standard - imgui-ref: 8067a7bb52a24e28ca4fbb7f35ff486d224ce91f + imgui-ref: 558339ea6c1977c2e18309e5a4b540058b659959 runs-on: ${{ matrix.os }} timeout-minutes: 45 env: @@ -62,7 +62,7 @@ jobs: Copy-Item build/volk/volk.h, build/volk/volk.c $Destination - name: Build, bundle and render Linux releases if: runner.os == 'Linux' - run: bash scripts/build_release_linux.sh '${{ matrix.variant }}' + run: v -prod run scripts/build_release.v '${{ matrix.variant }}' - name: Exercise bundled gallery through native AT-SPI if: runner.os == 'Linux' run: | @@ -71,7 +71,10 @@ jobs: - name: Build and audit Windows release DLLs if: runner.os == 'Windows' shell: pwsh - run: scripts/build_release_windows.ps1 -Variant '${{ matrix.variant }}' + run: | + . scripts/setup_windows_build.ps1 + v -prod run scripts/build_release.v '${{ matrix.variant }}' + if ($LASTEXITCODE -ne 0) { throw 'Portable release build failed' } - name: Exercise native Windows UI Automation if: runner.os == 'Windows' shell: pwsh @@ -118,7 +121,7 @@ jobs: - name: Build accessible touch app run: bash scripts/build_android.sh --build-only - name: Collect Android notices - run: bash scripts/collect_licenses.sh build/modules/antono2/imgui "build/android-touch-$ANDROID_ABI/licenses" android + run: v run scripts/collect_licenses.v build/modules/antono2/imgui "build/android-touch-$ANDROID_ABI/licenses" android - name: Upload unsigned APK and notices for release signing uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 with: diff --git a/IMGUI_REVISION b/IMGUI_REVISION index 793309e..79f94ec 100644 --- a/IMGUI_REVISION +++ b/IMGUI_REVISION @@ -1 +1 @@ -161bd5fa99d35fec1192b62cf22348ed785c3277 +93401818d99f80afe6b97c042f215cc4f11dc7de diff --git a/docs/releasing.md b/docs/releasing.md index 15e9359..c90be0a 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -7,11 +7,20 @@ Android inputs for three ABIs. Download the artifacts from that exact run. Retain the run URL and source commits in release notes. Do not publish CI debug APKs as release updates: their signing keys are temporary. +The release helpers are V programs with shared build, packaging, license and +ZIP code in `scripts/release_tools/`. From prepared dependencies, run +`v -prod run scripts/build_release.v docking` (or `standard`). On Windows first +load `. scripts/setup_windows_build.ps1` in PowerShell to select Visual Studio. +To package existing binaries, use `v -prod run scripts/package_release.v +binary-dir imgui-dir output-dir docking`. License collection uses +`v run scripts/collect_licenses.v imgui-dir output-dir`, with `android` as its +last argument for mobile notices. Rust, Cargo and Python are not required. + Desktop ZIPs are ready to promote after reviewing the checks. Android APKs are signed outside CI with the persistent private release key: ```sh -v run scripts/sign_android_release.v \ +v -prod run scripts/sign_android_release.v \ --input /path/to/android-release-input-armeabi-v7a \ --input /path/to/android-release-input-arm64-v8a \ --input /path/to/android-release-input-x86_64 \ @@ -25,6 +34,7 @@ The script rejects mismatched common APK content, combines the ABI libraries, embeds license notices, aligns the APKs, signs them and verifies each signature. Keep the key and password private and backed up outside the repository. Never upload them as release assets. Use the same signing identity for updates. +The private key uses the same password as the keystore. Increment `android:versionCode` and update `android:versionName` for each release. Test the final universal APK on a device, including the installation/update diff --git a/examples/widget_gallery/README.md b/examples/widget_gallery/README.md index 9326991..8cb3e9d 100644 --- a/examples/widget_gallery/README.md +++ b/examples/widget_gallery/README.md @@ -29,7 +29,6 @@ status/progress, and a virtual file list. Focus last file reveals the list and moves keyboard/native focus to its last row. Switch to Raw ImGui widgets for the original tables, popups and secondary windows. See [release installation](../../docs/installing-releases.md) for supported downloads. -Use `scripts/build_release_linux.sh docking` or -`scripts/build_release_windows.ps1 -Variant docking` to build the accessible +Use `v -prod run scripts/build_release.v docking` to build the accessible package from prepared dependencies (Windows uses a Visual Studio developer shell). The normal setup command remains the direct raw API build. diff --git a/scripts/build_release.v b/scripts/build_release.v new file mode 100644 index 0000000..508cfe6 --- /dev/null +++ b/scripts/build_release.v @@ -0,0 +1,109 @@ +module main + +import os +import release_tools + +fn find_file(root string, names []string, containing string) !string { + mut files := os.walk_ext(root, '') + files.sort() + for file in files { + if os.file_name(file) in names && file.contains(containing) { + return file + } + } + return error('Missing build output ${names} in ${root}') +} + +fn main() { + run() or { + eprintln(err) + exit(1) + } +} + +fn run() ! { + if os.args.len != 2 || os.args[1] !in ['docking', 'standard'] { + return error('Usage: v run scripts/build_release.v docking|standard') + } + variant := os.args[1] + root := os.dir(os.dir(@FILE)) + imgui := if os.getenv('IMGUI_DIR') != '' { + os.getenv('IMGUI_DIR') + } else { + os.join_path(root, 'build', 'modules', 'antono2', 'imgui') + } + if os.getenv('VMODULES') == '' { + os.setenv('VMODULES', os.join_path(root, 'build', 'modules'), true) + } + mut compiler := os.getenv('V_BIN') + if compiler == '' { + $if windows { + compiler = os.join_path(@VEXEROOT, 'v.exe') + } $else { + compiler = os.join_path(@VEXEROOT, 'v') + } + } + mut native_options := ['run', os.join_path(imgui, 'build_vimgui.vsh'), '--linkage', 'shared'] + mut native_directory := '' + $if windows { + native_options << ['--glfw', 'bundled', '--glfw-version', '3.4'] + native_directory = os.join_path(imgui, 'build', 'shared-bundled-3.4') + if os.getenv('VCToolsRedistDir') == '' { + return error('Load scripts/setup_windows_build.ps1 before running the builder') + } + } $else $if linux { + native_options << ['--glfw', 'system'] + native_directory = os.join_path(imgui, 'build', 'shared-system-3.3') + os.setenv('VULKAN_SDK', '/usr', true) + os.setenv('GLFW_INCLUDE', '/usr/include', true) + os.setenv('GLFW_LIB', '/usr/lib/x86_64-linux-gnu', true) + } $else { + return error('Release builder currently supports Linux and Windows') + } + release_tools.command(compiler, native_options)! + release_tools.command('cmake', ['-S', imgui, '-B', native_directory, '-DVIMGUI_APPLICATION_UI=ON', + '-DVIMGUI_OUTPUT_DIR=' + os.join_path(imgui, 'lib')])! + release_tools.command('cmake', ['--build', native_directory, '--config', 'Release', '--parallel', + '4'])! + binaries := os.join_path(root, 'build', 'release-binaries-' + variant) + os.mkdir_all(binaries)! + mut flags := ['-d', 'release_accessibility', '-d', 'appui_embedded', '-no-memory-limit'] + mut extension := '' + mut platform := '' + $if windows { + header := find_file(native_directory, ['glfw3.h'], 'glfw-src')! + library := find_file(native_directory, ['glfw3dll.lib', 'glfw3.lib'], '')! + links := os.join_path(os.getenv('RUNNER_TEMP'), 'link') + os.mkdir_all(links)! + release_tools.copy(library, os.join_path(links, 'glfw3.lib'))! + os.setenv('GLFW_INCLUDE', os.dir(os.dir(header)), true) + os.setenv('GLFW_LIB', links, true) + flags << ['-path', os.join_path(root, 'build', 'modules', 'antono2') + '|@vlib|@vmodules', + '-cc', 'msvc'] + release_tools.copy(find_file(native_directory, ['glfw3.dll'], '')!, os.join_path(binaries, 'glfw3.dll'))! + release_tools.copy(find_file(os.join_path(imgui, 'lib'), ['vimgui.dll'], '')!, os.join_path(binaries, 'vimgui.dll'))! + extension = '.exe' + platform = 'windows' + } $else { + flags << ['-cc', 'gcc'] + platform = 'linux' + } + for example in ['glfw_vulkan', 'widget_gallery', 'implot_dashboard'] { + source := if example == 'glfw_vulkan' { + root + } else { + os.join_path(root, 'examples', example) + } + mut args := flags.clone() + args << ['-o', os.join_path(binaries, example + extension), source] + release_tools.command(compiler, args)! + } + output := os.join_path(root, 'build', 'v-imgui-examples-${platform}-x64-${variant}') + release_tools.package_release(root, binaries, imgui, output, variant)! + $if linux { + release_tools.command('bash', [ + os.join_path(root, 'scripts', 'smoke_desktop.sh'), + output, + ])! + } +} diff --git a/scripts/build_release_linux.sh b/scripts/build_release_linux.sh deleted file mode 100755 index a6ff1ce..0000000 --- a/scripts/build_release_linux.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail -root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) -imgui=${IMGUI_DIR:-$root/build/modules/antono2/imgui} -variant=${1:?Usage: build_release_linux.sh docking|standard} -export VMODULES=${VMODULES:-$root/build/modules} -export VULKAN_SDK=/usr GLFW_INCLUDE=/usr/include GLFW_LIB=/usr/lib/x86_64-linux-gnu -v=${V_BIN:-v} -"$v" run "$imgui/build_vimgui.vsh" --linkage shared --glfw system -cmake -S "$imgui" -B "$imgui/build/shared-system-3.3" \ - -DVIMGUI_APPLICATION_UI=ON "-DVIMGUI_OUTPUT_DIR=$imgui/lib" -cmake --build "$imgui/build/shared-system-3.3" --parallel 4 -binaries="$root/build/release-binaries-$variant" -mkdir -p "$binaries" -for example in glfw_vulkan widget_gallery implot_dashboard; do - source=$root - if [[ $example != glfw_vulkan ]]; then source="$root/examples/$example"; fi - "$v" -d release_accessibility -d appui_embedded -no-memory-limit -cc gcc -o "$binaries/$example" "$source" -done -bash "$root/scripts/package_linux.sh" "$binaries" "$imgui" "$root/build/v-imgui-examples-linux-x64-$variant" "$variant" -# Check the extracted bundle from outside its original build directory. -bash "$root/scripts/smoke_desktop.sh" "$root/build/v-imgui-examples-linux-x64-$variant" diff --git a/scripts/build_release_windows.ps1 b/scripts/build_release_windows.ps1 deleted file mode 100644 index f61fc4d..0000000 --- a/scripts/build_release_windows.ps1 +++ /dev/null @@ -1,50 +0,0 @@ -param([Parameter(Mandatory)] [ValidateSet('docking', 'standard')] [string] $Variant) -$ErrorActionPreference = 'Stop' -$Root = Split-Path -Parent $PSScriptRoot -Set-Location $Root -$VsWhere = Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio\Installer\vswhere.exe' -$InstallPath = & $VsWhere -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath -if (-not $InstallPath) { throw 'Visual Studio C++ tools were not found' } -Import-Module (Join-Path $InstallPath 'Common7\Tools\Microsoft.VisualStudio.DevShell.dll') -Enter-VsDevShell -VsInstallPath $InstallPath -SkipAutomaticLocation -DevCmdArguments '-arch=x64 -host_arch=x64' - -$ImguiRoot = Join-Path $env:GITHUB_WORKSPACE 'build\modules\antono2\imgui' -v run (Join-Path $ImguiRoot 'build_vimgui.vsh') --linkage shared --glfw bundled --glfw-version 3.4 -if ($LASTEXITCODE -ne 0) { throw 'Could not build the native ImGui library' } - -$NativeBuild = Join-Path $ImguiRoot 'build\shared-bundled-3.4' -cmake -S $ImguiRoot -B $NativeBuild -DVIMGUI_APPLICATION_UI=ON -if ($LASTEXITCODE -ne 0) { throw 'Accessible native configure failed' } -cmake --build $NativeBuild --config Release --parallel 4 -if ($LASTEXITCODE -ne 0) { throw 'Accessible native build failed' } - -$GlfwHeader = Get-ChildItem $NativeBuild -Recurse -Filter 'glfw3.h' | - Where-Object { $_.FullName -match 'glfw-src.*include.GLFW' } | Select-Object -First 1 -$GlfwLibrary = Get-ChildItem $NativeBuild -Recurse -Include 'glfw3dll.lib', 'glfw3.lib' | Select-Object -First 1 -$GlfwDll = Get-ChildItem $NativeBuild -Recurse -Filter 'glfw3.dll' | Select-Object -First 1 -$VimguiDll = Get-ChildItem (Join-Path $ImguiRoot 'lib') -Recurse -Filter 'vimgui.dll' | Select-Object -First 1 -if (-not $GlfwHeader -or -not $GlfwLibrary -or -not $GlfwDll -or -not $VimguiDll) { - throw 'The native build did not produce all required Windows artifacts' -} - -$LinkDirectory = Join-Path $env:RUNNER_TEMP 'link' -New-Item -ItemType Directory -Force $LinkDirectory | Out-Null -Copy-Item $GlfwLibrary.FullName (Join-Path $LinkDirectory 'glfw3.lib') -$env:GLFW_INCLUDE = Split-Path -Parent (Split-Path -Parent $GlfwHeader.FullName) -$env:GLFW_LIB = $LinkDirectory -$Executable = Join-Path $env:RUNNER_TEMP 'v_imgui_demo.exe' -$ModulePath = "$env:GITHUB_WORKSPACE\build\modules\antono2|@vlib|@vmodules" -$Output = Join-Path $Root "build/release-binaries-$Variant" -New-Item -ItemType Directory -Force $Output | Out-Null -foreach ($Example in @('glfw_vulkan', 'widget_gallery', 'implot_dashboard')) { - $Source = if ($Example -eq 'glfw_vulkan') { '.' } else { "./examples/$Example" } - $Executable = Join-Path $Output "$Example.exe" - v -d release_accessibility -d appui_embedded -no-memory-limit -path $ModulePath -cc msvc -o $Executable $Source - if ($LASTEXITCODE -ne 0) { throw "The Windows $Example did not compile" } -} -Copy-Item $GlfwDll.FullName, $VimguiDll.FullName $Output -Force -foreach ($Artifact in @($Executable, $GlfwDll.FullName, $VimguiDll.FullName)) { - if (-not (Test-Path $Artifact)) { throw "Missing Windows artifact: $Artifact" } -} - -& (Join-Path $PSScriptRoot 'package_windows.ps1') -Binaries $Output -Imgui $ImguiRoot -Variant $Variant -Output (Join-Path $Root "build/v-imgui-examples-windows-x64-$Variant") diff --git a/scripts/collect_licenses.ps1 b/scripts/collect_licenses.ps1 deleted file mode 100644 index 40a5934..0000000 --- a/scripts/collect_licenses.ps1 +++ /dev/null @@ -1,21 +0,0 @@ -param([Parameter(Mandatory)] [string] $Imgui, [Parameter(Mandatory)] [string] $Output) -$ErrorActionPreference = 'Stop' -$Root = Split-Path -Parent $PSScriptRoot -New-Item -ItemType Directory -Force $Output | Out-Null -$Files = @{ - 'examples.txt' = "$Root/LICENSE" - 'imgui-v.txt' = "$Imgui/LICENSE" - 'imgui.txt' = "$Imgui/cimgui/imgui/LICENSE.txt" - 'cimgui.txt' = "$Imgui/cimgui/LICENSE" - 'implot.txt' = "$Imgui/cimplot/implot/LICENSE" - 'cimplot.txt' = "$Imgui/cimplot/LICENSE" - 'proggy.txt' = "$Root/packaging/PROGGY-LICENSE.txt" - 'proggyforever.txt' = "$Root/packaging/PROGGYFOREVER-LICENSE.txt" -} -foreach ($File in $Files.GetEnumerator()) { Copy-Item $File.Value (Join-Path $Output $File.Key) } -$VRoot = Split-Path -Parent (Get-Command v).Source -Copy-Item "$VRoot/LICENSE" (Join-Path $Output 'v.txt') -$Source = Get-Content -Raw "$VRoot/thirdparty/libgc/gc.c" -$Notices = [regex]::Matches($Source, '(?s)/\*.*?\*/') | ForEach-Object { $_.Value } -$Notices | Set-Content -Encoding utf8 (Join-Path $Output 'boehm-gc-notices.txt') -Copy-Item "$VRoot/thirdparty/libatomic_ops/LICENSE" (Join-Path $Output 'libatomic-ops.txt') diff --git a/scripts/collect_licenses.sh b/scripts/collect_licenses.sh deleted file mode 100755 index d1fdb38..0000000 --- a/scripts/collect_licenses.sh +++ /dev/null @@ -1,37 +0,0 @@ -#!/usr/bin/env bash -# Copy redistribution notices for the components actually used by the builds. -set -euo pipefail -root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) -imgui=${1:?Usage: collect_licenses.sh imgui-dir output-dir [android]} -output=${2:?Missing output directory} -mkdir -p "$output" -cp "$root/LICENSE" "$output/examples.txt" -cp "$imgui/LICENSE" "$output/imgui-v.txt" -cp "$imgui/cimgui/imgui/LICENSE.txt" "$output/imgui.txt" -cp "$imgui/cimgui/LICENSE" "$output/cimgui.txt" -cp "$imgui/cimplot/implot/LICENSE" "$output/implot.txt" -cp "$imgui/cimplot/LICENSE" "$output/cimplot.txt" -cp "$root/packaging/PROGGY-LICENSE.txt" "$root/packaging/PROGGYFOREVER-LICENSE.txt" "$output/" -if [[ ${3:-} == android ]]; then - cp "$root/packaging/ROBOTO-LICENSE.txt" "$output/" - freetype="$imgui/third_party/freetype" - cp "$freetype/LICENSE.TXT" "$output/freetype-license.txt" - cp "$freetype/docs/FTL.TXT" "$output/freetype-FTL.txt" - cp "$freetype/subprojects/dlg/LICENSE" "$output/freetype-dlg.txt" - sed -n '1,/\*\//p' "$freetype/src/gzip/zlib.h" > "$output/freetype-zlib.txt" - cat > "$output/freetype-attribution.txt" <<'NOTICE' -This app includes FreeType 2.13.3. Portions of this software are copyright -© 2024 The FreeType Project (www.freetype.org). All rights reserved. -FreeType is redistributed under the FreeType License (FTL). -NOTICE -fi -v_binary=${V_BIN:-$(command -v v)} -v_root=$(cd -- "$(dirname -- "$v_binary")" && pwd) -cp "$v_root/LICENSE" "$output/v.txt" -if [[ ${3:-} != android ]]; then - # The default desktop collector is statically linked. Retain every source - # comment, including component copyright and permission notices. - awk 'index($0,"/*") {block=1} block {print} index($0,"*/") {block=0}' \ - "$v_root/thirdparty/libgc/gc.c" > "$output/boehm-gc-notices.txt" - cp "$v_root/thirdparty/libatomic_ops/LICENSE" "$output/libatomic-ops.txt" -fi diff --git a/scripts/collect_licenses.v b/scripts/collect_licenses.v new file mode 100644 index 0000000..b8d3d90 --- /dev/null +++ b/scripts/collect_licenses.v @@ -0,0 +1,15 @@ +module main + +import os +import release_tools + +fn main() { + if os.args.len !in [3, 4] || (os.args.len == 4 && os.args[3] != 'android') { + eprintln('Usage: v run scripts/collect_licenses.v imgui-dir output-dir [android]') + exit(2) + } + release_tools.collect_licenses(os.dir(os.dir(@FILE)), os.args[1], os.args[2], os.args.len == 4) or { + eprintln(err) + exit(1) + } +} diff --git a/scripts/package_linux.sh b/scripts/package_linux.sh deleted file mode 100755 index c6736e5..0000000 --- a/scripts/package_linux.sh +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail -root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) -binaries=${1:?Usage: package_linux.sh binary-dir imgui-dir output-dir docking|standard} -imgui=${2:?Missing ImGui directory} -output=${3:?Missing output directory} -variant=${4:?Missing variant} -[[ $variant == docking || $variant == standard ]] -[[ ! -e $output ]] -mkdir -p "$output/lib" "$output/licenses" -output=$(cd "$output" && pwd) -queue=() -declare -A digests -base_library() { [[ $1 =~ ^(lib(c|m|pthread|dl|rt|resolv|util|mvec)\.so\.|ld-linux|linux-vdso) ]]; } -dependencies() { - local file=$1 line name destination - local listing - listing=$(LD_LIBRARY_PATH= ldd "$file") - if [[ $listing == *'not found'* ]]; then printf '%s\n' "$listing" >&2; return 1; fi - while IFS= read -r line; do - if [[ $line =~ ^[[:space:]]*([^[:space:]]+)[[:space:]]+\=\>[[:space:]]+(/[^[:space:]]+)[[:space:]] ]]; then - name=${BASH_REMATCH[1]}; destination=${BASH_REMATCH[2]} - if ! base_library "$name"; then queue+=("$name" "$destination"); fi - fi - done <<< "$listing" -} -for example in glfw_vulkan widget_gallery implot_dashboard; do - cp "$binaries/$example" "$output/$example" - dependencies "$binaries/$example" -done -# GLFW uses dlopen for extension libraries and Vulkan presentation. -for name in libvulkan.so.1 libXrandr.so.2 libXinerama.so.1 libXcursor.so.1 libXi.so.6 libXxf86vm.so.1; do - queue+=("$name" "/usr/lib/x86_64-linux-gnu/$name") -done -index=0 -while (( index < ${#queue[@]} )); do - name=${queue[index]}; file=${queue[index+1]}; index=$((index+2)) - digest=$(sha256sum "$file"); digest=${digest%% *} - if [[ -v digests[$name] ]]; then [[ ${digests[$name]} == "$digest" ]]; continue; fi - digests[$name]=$digest - cp -L "$file" "$output/lib/$name" - dependencies "$file" - for candidate in "$file" "$(readlink -f "$file")" "${file#/usr}"; do - while IFS= read -r owner; do - owner=${owner%%: *}; owner=${owner%%:*} - if [[ -f /usr/share/doc/$owner/copyright ]]; then - cp "/usr/share/doc/$owner/copyright" "$output/licenses/$owner.txt" - fi - done < <(dpkg-query -S "$candidate" 2>/dev/null || true) - done -done -bash "$root/scripts/collect_licenses.sh" "$imgui" "$output/licenses" -cp "$root/packaging/run.sh" "$output/run.sh" -chmod +x "$output/run.sh" -cp "$root/packaging/README-linux.txt" "$output/README.txt" -printf '%s\n' "$variant" > "$output/VARIANT.txt" -printf '%s\n' "${!digests[@]}" | sort > "$output/RUNTIME-LIBRARIES.txt" -for example in glfw_vulkan widget_gallery implot_dashboard; do - patchelf --set-rpath '$ORIGIN/lib' "$output/$example" -done -for file in "$output"/lib/*; do patchelf --set-rpath '$ORIGIN' "$file"; done -queue=() -for file in "$output"/glfw_vulkan "$output"/widget_gallery "$output"/implot_dashboard "$output"/lib/*; do dependencies "$file"; done -for ((index=0;index<${#queue[@]};index+=2)); do - [[ ${queue[index+1]} == "$output/lib/"* ]] || { printf 'Unbundled library: %s\n' "${queue[index+1]}" >&2; exit 1; } -done -# Store timestamps in the range supported by ZIP, including archive license files. -find "$output" -type f ! -newermt 1980-01-01 -exec touch -t 198001010000 {} + -(cd "$(dirname "$output")"; zip -q -r -9 "$(basename "$output").zip" "$(basename "$output")") -printf '%s.zip\n' "$output" diff --git a/scripts/package_release.v b/scripts/package_release.v new file mode 100644 index 0000000..9c364f2 --- /dev/null +++ b/scripts/package_release.v @@ -0,0 +1,15 @@ +module main + +import os +import release_tools + +fn main() { + if os.args.len != 5 { + eprintln('Usage: v run scripts/package_release.v binary-dir imgui-dir output-dir docking|standard') + exit(2) + } + release_tools.package_release(os.dir(os.dir(@FILE)), os.args[1], os.args[2], os.args[3], os.args[4]) or { + eprintln(err) + exit(1) + } +} diff --git a/scripts/package_windows.ps1 b/scripts/package_windows.ps1 deleted file mode 100644 index 7ddb8ae..0000000 --- a/scripts/package_windows.ps1 +++ /dev/null @@ -1,55 +0,0 @@ -param( - [Parameter(Mandatory)] [string] $Binaries, - [Parameter(Mandatory)] [string] $Imgui, - [Parameter(Mandatory)] [string] $Output, - [Parameter(Mandatory)] [ValidateSet('docking', 'standard')] [string] $Variant -) -$ErrorActionPreference = 'Stop' -$Root = Split-Path -Parent $PSScriptRoot -if (Test-Path $Output) { throw "Output already exists: $Output" } -New-Item -ItemType Directory $Output | Out-Null -$Licenses = Join-Path $Output 'licenses' -foreach ($Name in @('glfw_vulkan.exe', 'widget_gallery.exe', 'implot_dashboard.exe', 'vimgui.dll', 'glfw3.dll')) { - Copy-Item (Join-Path $Binaries $Name) $Output -} -# Deploy the Microsoft redistributable CRT beside the application. -if (-not $env:VCToolsRedistDir) { throw 'Run in the Visual Studio developer shell' } -$Crt = Get-ChildItem (Join-Path $env:VCToolsRedistDir 'x64') -Directory -Filter 'Microsoft.VC*.CRT' | Select-Object -First 1 -if (-not $Crt) { throw 'The redistributable x64 C runtime was not found' } -Get-ChildItem $Crt.FullName -Filter '*.dll' | Copy-Item -Destination $Output -Copy-Item (Join-Path $env:VULKAN_SDK 'Bin/vulkan-1.dll') $Output -& (Join-Path $PSScriptRoot 'collect_licenses.ps1') -Imgui $Imgui -Output $Licenses -$GlfwLicense = Get-ChildItem (Join-Path $Imgui 'build/shared-bundled-3.4') -Recurse -Filter 'LICENSE.md' | - Where-Object { $_.FullName -match 'glfw-src' } | Select-Object -First 1 -if (-not $GlfwLicense) { throw 'GLFW license not found' } -Copy-Item $GlfwLicense.FullName (Join-Path $Licenses 'glfw.txt') -Copy-Item (Join-Path $Root 'packaging/README-windows.txt') (Join-Path $Output 'README.txt') -Copy-Item (Join-Path $Root 'packaging/MICROSOFT-RUNTIME-NOTICE.txt') $Licenses -Copy-Item (Join-Path $Root 'packaging/VULKAN-LOADER-LICENSE.txt') $Licenses -$Variant | Set-Content (Join-Path $Output 'VARIANT.txt') -# Every imported non-OS DLL must exist beside the applications. Driver DLLs are -# loaded by the Vulkan loader at runtime from the installed graphics driver. -$Imports = @{} -foreach ($File in Get-ChildItem $Output -File | Where-Object { $_.Extension -in '.exe', '.dll' }) { - $Dump = & dumpbin /nologo /dependents $File.FullName - if ($LASTEXITCODE -ne 0) { throw "Cannot inspect $($File.Name)" } - foreach ($Line in $Dump) { - if ($Line -match '^\s+([a-zA-Z0-9_.-]+\.dll)\s*$') { - $Name = $Matches[1].ToLowerInvariant() - $Imports[$Name] = $true - if (Test-Path (Join-Path $Output $Name)) { continue } - $OsDlls = @('kernel32.dll','user32.dll','gdi32.dll','advapi32.dll','shell32.dll','ole32.dll', - 'oleaut32.dll','comdlg32.dll','shlwapi.dll','setupapi.dll','cfgmgr32.dll','ntdll.dll','ucrtbase.dll', - 'ws2_32.dll','bcrypt.dll','crypt32.dll','secur32.dll','rpcrt4.dll','dwmapi.dll','version.dll', - 'userenv.dll','imm32.dll','winmm.dll','msvcrt.dll','powrprof.dll','uxtheme.dll', - 'hid.dll','winspool.drv','uiautomationcore.dll','comctl32.dll','propsys.dll','netapi32.dll','mswsock.dll', - 'normaliz.dll','d3d11.dll','dxgi.dll','opengl32.dll') - if ($Name -notin $OsDlls -and $Name -notmatch '^(api|ext)-ms-') { - throw "Unbundled runtime dependency: $($File.Name) -> $Name" - } - } - } -} -$Imports.Keys | Sort-Object | Set-Content (Join-Path $Output 'RUNTIME-LIBRARIES.txt') -Compress-Archive -Path $Output -DestinationPath "$Output.zip" -CompressionLevel Optimal -Write-Output "$Output.zip" diff --git a/scripts/release_tools/common.v b/scripts/release_tools/common.v new file mode 100644 index 0000000..27bccd5 --- /dev/null +++ b/scripts/release_tools/common.v @@ -0,0 +1,41 @@ +module release_tools + +import os + +// Argument vectors keep paths and literal RPATH values out of a shell parser. +pub fn command(program string, args []string) ! { + mut process := os.new_process(program) + process.set_args(args) + process.run() + process.wait() + code := process.code + process.close() + if code != 0 { + return error('${program} failed (${code})') + } +} + +pub fn capture(program string, args []string) !(int, string) { + mut process := os.new_process(program) + process.set_args(args) + process.set_redirect_stdio_merged() + process.run() + output := process.stdout_slurp() + process.wait() + code := process.code + process.close() + return code, output +} + +pub fn checked_output(program string, args []string) !string { + code, output := capture(program, args)! + if code != 0 { + return error('${program} failed (${code}): ${output}') + } + return output +} + +pub fn copy(source string, destination string) ! { + os.mkdir_all(os.dir(destination))! + os.cp(os.real_path(source), destination)! +} diff --git a/scripts/release_tools/licenses.v b/scripts/release_tools/licenses.v new file mode 100644 index 0000000..172c9a1 --- /dev/null +++ b/scripts/release_tools/licenses.v @@ -0,0 +1,54 @@ +module release_tools + +import os + +// Keep the source's permission notices, including embedded component notices. +fn source_comments(source string) !string { + mut result := []string{} + mut offset := 0 + for offset < source.len { + begin := source[offset..].index('/*') or { break } + start := offset + begin + end := source[start + 2..].index('*/') or { return error('Unclosed source comment') } + offset = start + 2 + end + 2 + result << source[start..offset] + } + return result.join('\n\n') + '\n' +} + +pub fn collect_licenses(root string, imgui string, output string, android bool) ! { + files := { + 'examples.txt': os.join_path(root, 'LICENSE') + 'imgui-v.txt': os.join_path(imgui, 'LICENSE') + 'imgui.txt': os.join_path(imgui, 'cimgui', 'imgui', 'LICENSE.txt') + 'cimgui.txt': os.join_path(imgui, 'cimgui', 'LICENSE') + 'implot.txt': os.join_path(imgui, 'cimplot', 'implot', 'LICENSE') + 'cimplot.txt': os.join_path(imgui, 'cimplot', 'LICENSE') + 'proggy.txt': os.join_path(root, 'packaging', 'PROGGY-LICENSE.txt') + 'proggyforever.txt': os.join_path(root, 'packaging', 'PROGGYFOREVER-LICENSE.txt') + 'v.txt': os.join_path(@VEXEROOT, 'LICENSE') + } + os.mkdir_all(output)! + for name, source in files { + copy(source, os.join_path(output, name))! + } + if android { + freetype := os.join_path(imgui, 'third_party', 'freetype') + for name, source in { + 'roboto.txt': os.join_path(root, 'packaging', 'ROBOTO-LICENSE.txt') + 'freetype-license.txt': os.join_path(freetype, 'LICENSE.TXT') + 'freetype-FTL.txt': os.join_path(freetype, 'docs', 'FTL.TXT') + 'freetype-dlg.txt': os.join_path(freetype, 'subprojects', 'dlg', 'LICENSE') + } { + copy(source, os.join_path(output, name))! + } + zlib := os.read_file(os.join_path(freetype, 'src', 'gzip', 'zlib.h'))! + end := zlib.index('*/') or { return error('Missing FreeType zlib notice') } + os.write_file(os.join_path(output, 'freetype-zlib.txt'), zlib[..end + 2] + '\n')! + os.write_file(os.join_path(output, 'freetype-attribution.txt'), 'This app includes FreeType 2.13.3. Portions of this software are copyright\n© 2024 The FreeType Project (www.freetype.org). All rights reserved.\nFreeType is redistributed under the FreeType License (FTL).\n')! + } else { + gc_source := os.read_file(os.join_path(@VEXEROOT, 'thirdparty', 'libgc', 'gc.c'))! + os.write_file(os.join_path(output, 'boehm-gc-notices.txt'), source_comments(gc_source)!)! + copy(os.join_path(@VEXEROOT, 'thirdparty', 'libatomic_ops', 'LICENSE'), os.join_path(output, 'libatomic-ops.txt'))! + } +} diff --git a/scripts/release_tools/package.v b/scripts/release_tools/package.v new file mode 100644 index 0000000..95445e5 --- /dev/null +++ b/scripts/release_tools/package.v @@ -0,0 +1,245 @@ +module release_tools + +import os +import crypto.sha256 + +const examples = ['glfw_vulkan', 'widget_gallery', 'implot_dashboard'] + +struct Library { + name string + path string +} + +fn os_library(name string) bool { + for base in ['libc.so.', 'libm.so.', 'libpthread.so.', 'libdl.so.', 'librt.so.', 'libresolv.so.', + 'libutil.so.', 'libmvec.so.', 'ld-linux', 'linux-vdso'] { + if name.starts_with(base) { + return true + } + } + return false +} + +fn linux_dependencies(file string) ![]Library { + output := checked_output('ldd', [file])! + if output.contains('not found') { + return error('Unresolved dependency: ${file}\n${output}') + } + mut result := []Library{} + for line in output.split_into_lines() { + if !line.contains('=>') { + continue + } + parts := line.split('=>') + name := parts[0].trim_space() + destination := parts[1].all_before_last(' (').trim_space() + if destination.starts_with('/') && !os_library(name) { + result << Library{ name: name, path: destination } + } + } + return result +} + +fn debian_notice(path string, output string) ! { + for candidate in [path, os.real_path(path), path.trim_string_left('/usr')] { + code, owners := capture('dpkg-query', ['-S', candidate])! + if code != 0 { + continue + } + for line in owners.split_into_lines() { + owner := line.all_before(': ').all_before(':') + notice := os.join_path('/usr/share/doc', owner, 'copyright') + if os.is_file(notice) { copy(notice, os.join_path(output, owner + '.txt'))! } + } + } +} + +fn package_linux(root string, binaries string, imgui string, output string) ![]string { + prior := os.getenv('LD_LIBRARY_PATH') + os.setenv('LD_LIBRARY_PATH', '', true) + defer { os.setenv('LD_LIBRARY_PATH', prior, true) } + library_dir := os.join_path(output, 'lib') + os.mkdir_all(library_dir)! + mut queue := []Library{} + for example in examples { + source := os.join_path(binaries, example) + copy(source, os.join_path(output, example))! + os.chmod(os.join_path(output, example), 0o755)! + queue << linux_dependencies(source)! + } + // GLFW loads extension libraries and Vulkan presentation with dlopen. + for name in ['libvulkan.so.1', 'libXrandr.so.2', 'libXinerama.so.1', 'libXcursor.so.1', + 'libXi.so.6', 'libXxf86vm.so.1'] { + queue << Library{ name: name, path: os.join_path('/usr/lib/x86_64-linux-gnu', name) } + } + mut digests := map[string]string{} + mut index := 0 + for index < queue.len { + library := queue[index] + index++ + digest := sha256.sum(os.read_bytes(library.path)!).hex() + if library.name in digests { + if digests[library.name] != digest { + return error('Conflicting library contents: ${library.name}') + } + continue + } + digests[library.name] = digest + copy(library.path, os.join_path(library_dir, library.name))! + queue << linux_dependencies(library.path)! + debian_notice(library.path, os.join_path(output, 'licenses'))! + } + collect_licenses(root, imgui, os.join_path(output, 'licenses'), false)! + copy(os.join_path(root, 'packaging', 'run.sh'), os.join_path(output, 'run.sh'))! + os.chmod(os.join_path(output, 'run.sh'), 0o755)! + copy(os.join_path(root, 'packaging', 'README-linux.txt'), os.join_path(output, 'README.txt'))! + mut audit_files := []string{} + for example in examples { + file := os.join_path(output, example) + command('patchelf', ['--set-rpath', r'$ORIGIN/lib', file])! + audit_files << file + } + for name in digests.keys() { + file := os.join_path(library_dir, name) + command('patchelf', ['--set-rpath', r'$ORIGIN', file])! + audit_files << file + } + for file in audit_files { + for dependency in linux_dependencies(file)! { + if !dependency.path.starts_with(library_dir + os.path_separator) { + return error('Unbundled library: ${dependency.path}') + } + } + } + return digests.keys() +} + +fn windows_os_library(name string) bool { + return name.starts_with('api-ms-') || name.starts_with('ext-ms-') || name in [ + 'kernel32.dll', + 'user32.dll', + 'gdi32.dll', + 'advapi32.dll', + 'shell32.dll', + 'ole32.dll', + 'oleaut32.dll', + 'comdlg32.dll', + 'shlwapi.dll', + 'setupapi.dll', + 'cfgmgr32.dll', + 'ntdll.dll', + 'ucrtbase.dll', + 'ws2_32.dll', + 'bcrypt.dll', + 'crypt32.dll', + 'secur32.dll', + 'rpcrt4.dll', + 'dwmapi.dll', + 'version.dll', + 'userenv.dll', + 'imm32.dll', + 'winmm.dll', + 'msvcrt.dll', + 'powrprof.dll', + 'uxtheme.dll', + 'hid.dll', + 'winspool.drv', + 'uiautomationcore.dll', + 'comctl32.dll', + 'propsys.dll', + 'netapi32.dll', + 'mswsock.dll', + 'normaliz.dll', + 'd3d11.dll', + 'dxgi.dll', + 'opengl32.dll', + ] +} + +fn package_windows(root string, binaries string, imgui string, output string) ![]string { + for example in examples { + copy(os.join_path(binaries, example + '.exe'), os.join_path(output, example + '.exe'))! + } + for name in ['vimgui.dll', 'glfw3.dll'] { + copy(os.join_path(binaries, name), os.join_path(output, name))! + } + redist := os.getenv('VCToolsRedistDir') + if redist == '' { + return error('Run in the Visual Studio developer shell') + } + mut crt := '' + for directory in os.ls(os.join_path(redist, 'x64'))! { + if directory.starts_with('Microsoft.VC') && directory.ends_with('.CRT') { + crt = os.join_path(redist, 'x64', directory) + break + } + } + if crt == '' { + return error('The redistributable x64 C runtime was not found') + } + for file in os.ls(crt)! { + if file.to_lower().ends_with('.dll') { copy(os.join_path(crt, file), os.join_path(output, file))! } + } + copy(os.join_path(os.getenv('VULKAN_SDK'), 'Bin', 'vulkan-1.dll'), os.join_path(output, 'vulkan-1.dll'))! + notices := os.join_path(output, 'licenses') + collect_licenses(root, imgui, notices, false)! + mut glfw_license := '' + for file in os.walk_ext(os.join_path(imgui, 'build', 'shared-bundled-3.4'), 'LICENSE.md') { + if file.contains('glfw-src') { + glfw_license = file + break + } + } + if glfw_license == '' { + return error('GLFW license not found') + } + copy(glfw_license, os.join_path(notices, 'glfw.txt'))! + copy(os.join_path(root, 'packaging', 'README-windows.txt'), os.join_path(output, 'README.txt'))! + for file in ['MICROSOFT-RUNTIME-NOTICE.txt', 'VULKAN-LOADER-LICENSE.txt'] { + copy(os.join_path(root, 'packaging', file), os.join_path(notices, file))! + } + mut imports := map[string]bool{} + files := os.ls(output)! + bundled := files.map(it.to_lower()) + for file in files { + if !file.to_lower().ends_with('.exe') && !file.to_lower().ends_with('.dll') { + continue + } + listing := checked_output('dumpbin', ['/nologo', '/dependents', os.join_path(output, file)])! + for line in listing.split_into_lines() { + name := line.trim_space().to_lower() + if !name.ends_with('.dll') || name.contains(' ') || name.contains('\t') { + continue + } + imports[name] = true + if name !in bundled && !windows_os_library(name) { + return error('Unbundled runtime dependency: ${file} -> ${name}') + } + } + } + return imports.keys() +} + +pub fn package_release(root string, binaries string, imgui string, destination string, variant string) ! { + if variant !in ['docking', 'standard'] { + return error('Variant must be docking or standard') + } + if os.exists(destination) || os.exists(destination + '.zip') { + return error('Output already exists: ${destination}') + } + os.mkdir_all(os.join_path(destination, 'licenses'))! + output := os.real_path(destination) + mut runtime := []string{} + $if windows { + runtime = package_windows(root, binaries, imgui, output)! + } $else $if linux { + runtime = package_linux(root, binaries, imgui, output)! + } $else { + return error('Portable packages currently support Linux and Windows') + } + runtime.sort() + os.write_file(os.join_path(output, 'RUNTIME-LIBRARIES.txt'), runtime.join('\n') + '\n')! + os.write_file(os.join_path(output, 'VARIANT.txt'), variant + '\n')! + zip_folder(output, output + '.zip')! + println(output + '.zip') +} diff --git a/scripts/release_tools/zip.v b/scripts/release_tools/zip.v new file mode 100644 index 0000000..d96a023 --- /dev/null +++ b/scripts/release_tools/zip.v @@ -0,0 +1,145 @@ +module release_tools + +import os +import compress.deflate +import hash.crc32 + +struct ZipEntry { + name string + crc u32 + compressed u32 + original u32 + offset u32 + mode u32 +} + +pub struct ZipWriter { +mut: + file os.File + offset u64 + entries []ZipEntry +} + +fn le16(mut bytes []u8, value u16) { + bytes << u8(value) + bytes << u8(value >> 8) +} + +fn le32(mut bytes []u8, value u32) { + for shift in [0, 8, 16, 24] { + bytes << u8(value >> shift) + } +} + +pub fn new_zip(path string) !ZipWriter { + return ZipWriter{ file: os.create(path)! } +} + +pub fn (mut zip ZipWriter) close() { + zip.file.close() +} + +fn (mut zip ZipWriter) write(bytes []u8) ! { + if zip.offset + u64(bytes.len) >= 0xffffffff { + return error('ZIP32 archive exceeds 4 GiB') + } + written := zip.file.write(bytes)! + if written != bytes.len { + return error('Incomplete ZIP write') + } + zip.offset += u64(written) +} + +// APK tools require ZIP32. szip's writer always emits ZIP64, so share this +// small writer between APK signing and desktop packages. Fixed DOS timestamps +// avoid invalid dates from source archives and make entry metadata reproducible. +pub fn (mut zip ZipWriter) add(name string, bytes []u8, mode u32) ! { + if name == '' || name.starts_with('/') || name.contains('\\') || name.split('/').contains('..') || name.len > 65535 { + return error('Invalid ZIP entry: ${name}') + } + if zip.entries.len >= 65535 || u64(bytes.len) >= 0xffffffff { + return error('ZIP32 limit exceeded') + } + compressed := deflate.compress(bytes, format: .raw_deflate)! + entry := ZipEntry{ name: name, crc: crc32.sum(bytes), compressed: u32(compressed.len), original: u32(bytes.len), offset: u32(zip.offset), mode: mode } + mut header := []u8{} + le32(mut header, 0x04034b50) + le16(mut header, 20) + le16(mut header, 0x0800) + le16(mut header, 8) + le16(mut header, 0) + le16(mut header, 0x0021) + le32(mut header, entry.crc) + le32(mut header, entry.compressed) + le32(mut header, entry.original) + le16(mut header, u16(name.len)) + le16(mut header, 0) + header << name.bytes() + zip.write(header)! + zip.write(compressed)! + zip.entries << entry +} + +pub fn (mut zip ZipWriter) finish() ! { + central_offset := u32(zip.offset) + for entry in zip.entries { + mut header := []u8{} + le32(mut header, 0x02014b50) + le16(mut header, 0x0314) + le16(mut header, 20) + le16(mut header, 0x0800) + le16(mut header, 8) + le16(mut header, 0) + le16(mut header, 0x0021) + le32(mut header, entry.crc) + le32(mut header, entry.compressed) + le32(mut header, entry.original) + le16(mut header, u16(entry.name.len)) + le16(mut header, 0) + le16(mut header, 0) + le16(mut header, 0) + le16(mut header, 0) + le32(mut header, entry.mode << 16) + le32(mut header, entry.offset) + header << entry.name.bytes() + zip.write(header)! + } + central_size := u32(zip.offset) - central_offset + mut end := []u8{} + le32(mut end, 0x06054b50) + le16(mut end, 0) + le16(mut end, 0) + le16(mut end, u16(zip.entries.len)) + le16(mut end, u16(zip.entries.len)) + le32(mut end, central_size) + le32(mut end, central_offset) + le16(mut end, 0) + zip.write(end)! +} + +pub fn zip_folder(folder string, archive string) ! { + mut zip := new_zip(archive)! + defer { zip.close() } + mut files := os.walk_ext(folder, '') + files.sort() + for file in files { + if !os.is_file(file) { + continue + } + relative := file.trim_string_left(folder + os.path_separator).replace('\\', '/') + info := os.stat(file)! + zip.add(os.file_name(folder) + '/' + relative, os.read_bytes(file)!, u32(0o100000) | (info.mode & 0o777))! + } + zip.finish()! +} + +pub fn zip_entries(path string, entries map[string][]u8) ! { + mut zip := new_zip(path)! + defer { zip.close() } + mut names := entries.keys() + names.sort() + for name in names { + zip.add(name, entries[name], 0o100644)! + } + zip.finish()! +} diff --git a/scripts/setup_windows_build.ps1 b/scripts/setup_windows_build.ps1 new file mode 100644 index 0000000..f64f53d --- /dev/null +++ b/scripts/setup_windows_build.ps1 @@ -0,0 +1,7 @@ +# Load the platform toolchain environment for the shared V release builder. +$ErrorActionPreference = 'Stop' +$VsWhere = Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio\Installer\vswhere.exe' +$InstallPath = & $VsWhere -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath +if (-not $InstallPath) { throw 'Visual Studio C++ tools were not found' } +Import-Module (Join-Path $InstallPath 'Common7\Tools\Microsoft.VisualStudio.DevShell.dll') +Enter-VsDevShell -VsInstallPath $InstallPath -SkipAutomaticLocation -DevCmdArguments '-arch=x64 -host_arch=x64' diff --git a/scripts/sign_android_release.v b/scripts/sign_android_release.v index a4ae095..7164db1 100644 --- a/scripts/sign_android_release.v +++ b/scripts/sign_android_release.v @@ -5,18 +5,7 @@ import os import rand import compress.szip import crypto.sha256 - -fn command(program string, args []string) ! { - mut process := os.new_process(program) - process.set_args(args) - process.run() - process.wait() - code := process.code - process.close() - if code != 0 { - return error('${program} failed (${code})') - } -} +import release_tools fn archive_entries(path string) !map[string][]u8 { mut archive := szip.open(path, .no_compression, .read_only)! @@ -141,23 +130,28 @@ fn run() ! { contents[name] = bytes } } - mut archive := szip.open(unsigned, .best_compression, .write)! - mut names := contents.keys() - names.sort() - for name in names { - archive.open_entry(name)! - archive.write_entry(contents[name])! - archive.close_entry() - } - archive.close() - command(os.join_path(options['--build-tools'], 'zipalign'), ['-f', '-p', '4', unsigned, - aligned])! + release_tools.zip_entries(unsigned, contents)! + release_tools.command(os.join_path(options['--build-tools'], 'zipalign'), ['-f', '-p', '4', + unsigned, aligned])! alias := if options['--alias'] != '' { options['--alias'] } else { 'v-imgui-release' } password := 'file:' + options['--password-file'] - command(os.join_path(options['--build-tools'], 'apksigner'), ['sign', '--ks', - options['--keystore'], '--ks-key-alias', alias, '--ks-pass', password, '--key-pass', - password, '--out', output, aligned])! - command(os.join_path(options['--build-tools'], 'apksigner'), ['verify', '--verbose', output])! + release_tools.command(os.join_path(options['--build-tools'], 'apksigner'), [ + 'sign', + '--ks', + options['--keystore'], + '--ks-key-alias', + alias, + '--ks-pass', + password, + '--out', + output, + aligned, + ])! + release_tools.command(os.join_path(options['--build-tools'], 'apksigner'), [ + 'verify', + '--verbose', + output, + ])! println('${sha256.hexhash(os.read_file(output)!)} ${os.file_name(output)}') } } From 69d58a413eebbd8361c86d6313629650885dec07 Mon Sep 17 00:00:00 2001 From: antono2 Date: Sat, 3 Oct 2026 06:50:20 +0200 Subject: [PATCH 06/29] Include portable launcher and support testing signed Android releases --- .gitignore | 1 + docs/testing.md | 10 +++++++++- packaging/run.sh | 7 +++++++ scripts/test_android_accessibility.sh | 9 +++++++-- 4 files changed, 24 insertions(+), 3 deletions(-) create mode 100755 packaging/run.sh diff --git a/.gitignore b/.gitignore index 9162275..cf25904 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,7 @@ bin/ imgui.ini main.cpp run.sh +!packaging/run.sh tmp.txt .editorconfig diff --git a/docs/testing.md b/docs/testing.md index c7f384b..dbe1e66 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -63,7 +63,9 @@ The portable workflow renders each Linux bundle after copying its dependencies and exercises the gallery through the real AT-SPI bus, including native roles, button actions, status, high contrast, 200% text, off-screen list focus/selection and view switching. Windows bundles are checked for missing imported runtime -DLLs. Graphical Windows/UI Automation behavior still needs a Windows user session. +DLLs. The Windows workflow also opens the packaged gallery in a runner's user +session and checks UI Automation actions, text/selection, list selection and +exact scroll percentages. Narrator speech still needs manual validation. On the connected Android tablet, the integrated accessibility build passed native roles/actions, checkbox state, progress ranges, Unicode text and UTF-16 selection, @@ -71,3 +73,9 @@ full-field clipboard copy/read and feedback, zoom, scrolling from static text, and resume state. Repeat with the final release APK and test portrait/landscape, visible system navigation bars and IME. Check TalkBack spoken navigation manually; provider action tests do not establish the quality of spoken announcements. + +To run instrumentation against a signed release APK, set +`VIMGUI_ANDROID_TEST_APK`, `VIMGUI_ANDROID_TEST_KEYSTORE` and +`VIMGUI_ANDROID_TEST_PASSWORD_FILE` before running +`scripts/test_android_accessibility.sh`. A debug installation with a different +certificate must be removed first; uninstalling it removes its saved demo data. diff --git a/packaging/run.sh b/packaging/run.sh new file mode 100755 index 0000000..97d4d89 --- /dev/null +++ b/packaging/run.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +set -euo pipefail +package_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +example=${1:-widget_gallery} +case "$example" in glfw_vulkan|widget_gallery|implot_dashboard) ;; *) echo 'Usage: run.sh [glfw_vulkan|widget_gallery|implot_dashboard]' >&2; exit 2 ;; esac +if [[ $# -gt 0 ]]; then shift; fi +exec "$package_dir/$example" "$@" diff --git a/scripts/test_android_accessibility.sh b/scripts/test_android_accessibility.sh index 2d89966..ba6d294 100755 --- a/scripts/test_android_accessibility.sh +++ b/scripts/test_android_accessibility.sh @@ -19,13 +19,18 @@ mapfile -d '' class_files < <(find "$package/classes" -name '*.class' -print0) -I "$android_jar" -F "$package/unsigned.apk" (cd "$package/dex"; "$build_tools/aapt" add "$package/unsigned.apk" classes.dex) "$build_tools/zipalign" -f 4 "$package/unsigned.apk" "$package/aligned.apk" -"$build_tools/apksigner" sign --ks "$build/debug.keystore" --ks-pass pass:android --key-pass pass:android \ +keystore=${VIMGUI_ANDROID_TEST_KEYSTORE:-$build/debug.keystore} +password=(--ks-pass pass:android) +if [[ -n ${VIMGUI_ANDROID_TEST_PASSWORD_FILE:-} ]]; then + password=(--ks-pass "file:$VIMGUI_ANDROID_TEST_PASSWORD_FILE") +fi +"$build_tools/apksigner" sign --ks "$keystore" "${password[@]}" \ --out "$package/test.apk" "$package/aligned.apk" "$build_tools/apksigner" verify "$package/test.apk" "${adb[@]}" shell input keyevent 224 "${adb[@]}" shell wm dismiss-keyguard "${adb[@]}" shell am force-stop io.antono2.vimgui.examples.touch -"${adb[@]}" install -r "$build/vimgui-demo-$abi.apk" +"${adb[@]}" install -r "${VIMGUI_ANDROID_TEST_APK:-$build/vimgui-demo-$abi.apk}" "${adb[@]}" install -r "$package/test.apk" "${adb[@]}" shell am instrument -w \ io.antono2.vimgui.examples.touch.test/io.antono2.vimgui.examples.touch.test.AccessibilitySmoke | tee "$package/instrumentation.log" From 0ad9ea34241fda5b02d65190aea1746d47810529 Mon Sep 17 00:00:00 2001 From: antono2 Date: Sat, 3 Oct 2026 07:01:06 +0200 Subject: [PATCH 07/29] List only relevant tools in release documentation --- docs/installing-releases.md | 2 +- docs/releasing.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/installing-releases.md b/docs/installing-releases.md index f75e121..964190b 100644 --- a/docs/installing-releases.md +++ b/docs/installing-releases.md @@ -2,7 +2,7 @@ Download assets from [Releases](https://github.com/antono2/v_imgui_examples/releases). The downloads include their application libraries. You do not need to install -V, CMake, Rust, GLFW, an Android SDK, or a separate Visual C++ runtime. +V, CMake, GLFW, an Android SDK, or a separate Visual C++ runtime. Your operating system, graphical desktop and graphics driver still provide the platform services. The examples use Vulkan; a supported Vulkan graphics driver is required. diff --git a/docs/releasing.md b/docs/releasing.md index c90be0a..80c7a2c 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -14,7 +14,7 @@ load `. scripts/setup_windows_build.ps1` in PowerShell to select Visual Studio. To package existing binaries, use `v -prod run scripts/package_release.v binary-dir imgui-dir output-dir docking`. License collection uses `v run scripts/collect_licenses.v imgui-dir output-dir`, with `android` as its -last argument for mobile notices. Rust, Cargo and Python are not required. +last argument for mobile notices. Desktop ZIPs are ready to promote after reviewing the checks. Android APKs are signed outside CI with the persistent private release key: From 09c14d13cf2519e2eb11a9b1532f4bb57cc84dfd Mon Sep 17 00:00:00 2001 From: antono2 Date: Sat, 3 Oct 2026 07:34:51 +0200 Subject: [PATCH 08/29] Resolve release commands across platforms and refresh native pins --- .github/workflows/release-packages.yml | 4 ++-- IMGUI_REVISION | 2 +- scripts/release_tools/common.v | 21 +++++++++++++++++++-- scripts/sign_android_release.v | 8 ++++++-- 4 files changed, 28 insertions(+), 7 deletions(-) diff --git a/.github/workflows/release-packages.yml b/.github/workflows/release-packages.yml index 0e8e118..c71b343 100644 --- a/.github/workflows/release-packages.yml +++ b/.github/workflows/release-packages.yml @@ -14,9 +14,9 @@ jobs: variant: [docking, standard] include: - variant: docking - imgui-ref: 93401818d99f80afe6b97c042f215cc4f11dc7de + imgui-ref: 047f2d94dbde25fc314fb3e0ed8fd07243d6c7f6 - variant: standard - imgui-ref: 558339ea6c1977c2e18309e5a4b540058b659959 + imgui-ref: 80fda7761bb3dd06b1ef5512297a5e51f74805d9 runs-on: ${{ matrix.os }} timeout-minutes: 45 env: diff --git a/IMGUI_REVISION b/IMGUI_REVISION index 79f94ec..59d2ba4 100644 --- a/IMGUI_REVISION +++ b/IMGUI_REVISION @@ -1 +1 @@ -93401818d99f80afe6b97c042f215cc4f11dc7de +047f2d94dbde25fc314fb3e0ed8fd07243d6c7f6 diff --git a/scripts/release_tools/common.v b/scripts/release_tools/common.v index 27bccd5..ead4a00 100644 --- a/scripts/release_tools/common.v +++ b/scripts/release_tools/common.v @@ -2,9 +2,26 @@ module release_tools import os +fn executable(program string) !string { + if os.is_file(program) { + return os.real_path(program) + } + $if windows { + if os.is_abs_path(program) || program.contains('/') || program.contains('\\') { + for suffix in ['.exe', '.bat', '.cmd'] { + if os.is_file(program + suffix) { + return os.real_path(program + suffix) + } + } + return error('Executable not found: ${program}') + } + } + return os.find_abs_path_of_executable(program) +} + // Argument vectors keep paths and literal RPATH values out of a shell parser. pub fn command(program string, args []string) ! { - mut process := os.new_process(program) + mut process := os.new_process(executable(program)!) process.set_args(args) process.run() process.wait() @@ -16,7 +33,7 @@ pub fn command(program string, args []string) ! { } pub fn capture(program string, args []string) !(int, string) { - mut process := os.new_process(program) + mut process := os.new_process(executable(program)!) process.set_args(args) process.set_redirect_stdio_merged() process.run() diff --git a/scripts/sign_android_release.v b/scripts/sign_android_release.v index 7164db1..3ee30ea 100644 --- a/scripts/sign_android_release.v +++ b/scripts/sign_android_release.v @@ -135,7 +135,9 @@ fn run() ! { unsigned, aligned])! alias := if options['--alias'] != '' { options['--alias'] } else { 'v-imgui-release' } password := 'file:' + options['--password-file'] - release_tools.command(os.join_path(options['--build-tools'], 'apksigner'), [ + release_tools.command('java', [ + '-jar', + os.join_path(options['--build-tools'], 'lib', 'apksigner.jar'), 'sign', '--ks', options['--keystore'], @@ -147,7 +149,9 @@ fn run() ! { output, aligned, ])! - release_tools.command(os.join_path(options['--build-tools'], 'apksigner'), [ + release_tools.command('java', [ + '-jar', + os.join_path(options['--build-tools'], 'lib', 'apksigner.jar'), 'verify', '--verbose', output, From 75e69dffe3c037fdb6d6184775ce11451a70bd44 Mon Sep 17 00:00:00 2001 From: antono2 Date: Sat, 3 Oct 2026 07:38:35 +0200 Subject: [PATCH 09/29] Guide example setup and add a portable release launcher --- .github/workflows/release-packages.yml | 2 + README.md | 48 +++-- docs/installing-releases.md | 10 +- examples/android_touch/README.md | 8 + packaging/README-linux.txt | 4 + packaging/README-windows.txt | 4 + packaging/examples.v | 50 +++++ packaging/run.sh | 3 +- scripts/release_tools/package.v | 20 +- scripts/smoke_desktop.sh | 4 +- setup.vsh | 253 ++++++++++++++++++++++--- 11 files changed, 356 insertions(+), 50 deletions(-) create mode 100644 packaging/examples.v diff --git a/.github/workflows/release-packages.yml b/.github/workflows/release-packages.yml index c71b343..2d8f2c4 100644 --- a/.github/workflows/release-packages.yml +++ b/.github/workflows/release-packages.yml @@ -75,6 +75,8 @@ jobs: . scripts/setup_windows_build.ps1 v -prod run scripts/build_release.v '${{ matrix.variant }}' if ($LASTEXITCODE -ne 0) { throw 'Portable release build failed' } + & "build/v-imgui-examples-windows-x64-${{ matrix.variant }}/examples.exe" --help + if ($LASTEXITCODE -ne 0) { throw 'Examples launcher failed' } - name: Exercise native Windows UI Automation if: runner.os == 'Windows' shell: pwsh diff --git a/README.md b/README.md index 3036711..c3e826f 100644 --- a/README.md +++ b/README.md @@ -19,23 +19,43 @@ with Vulkan validation in both standard and docking variants; Windows compiles all desktop examples. Android CI builds debug APKs for three ABIs. Device testing is required before release; see [the testing checklist](docs/testing.md). -## Run a release download +## Try a release [Download a release](https://github.com/antono2/v_imgui_examples/releases) and follow [installation instructions](docs/installing-releases.md). Desktop ZIPs bundle their application runtime libraries; Android APKs include their native libraries and font. The gallery and Android app include labelled native accessibility -controls, high contrast and text sizing. +controls, high contrast and text sizing. Choose **Docking** unless you specifically +want the Standard variant. + +- **Windows:** extract the ZIP and open `examples.exe` to choose an example. +- **Linux:** extract the ZIP and run `./run.sh` for the example menu. +- **Android:** install the universal APK and open **V ImGui Touch Examples**. ## Build from source -From this checkout, the cross-platform setup installs/builds ImGui and compiles -the example without opening a window: +Install V and Git, then run this command from the checkout: ```sh v run setup.vsh -v run setup.vsh --example widget_gallery -v run setup.vsh --example implot_dashboard +``` + +Use the official V compiler. The tested compiler revisions are recorded in +[the CI compiler setup](.github/actions/setup-v/action.yml). + +The guided runner lets you choose an example, explains setup changes, builds it +and opens it. It streams build progress and offers to run an existing desktop +build or rebuild it on later visits. Widget gallery is the default choice. +Missing tools are reported with next steps. System package installation requires +confirmation in the guided flow. + +For automation, explicit options skip the menus and setup confirmation. System +installers may still request administrator authorization: + +```sh +v run setup.vsh --install --example widget_gallery --run +v run setup.vsh --build-only --example implot_dashboard --run +v run setup.vsh --check --example widget_gallery ``` Setup uses an isolated `build/modules` dependency directory and checks out the @@ -43,14 +63,16 @@ ImGui commit recorded in `IMGUI_REVISION`. It does not discard changes in an existing dependency checkout. Set `VMODULES` to use a different module directory. The setup may install system prerequisites through the upstream setup script. -On Linux/macOS, launch `build/glfw_vulkan`, `build/widget_gallery`, or -`build/implot_dashboard` after setup. Windows setup reports its executable path. -Use `--build-only` to compile using dependencies already prepared, or `--check` -for read-only prerequisite diagnostics. Both accept `--example`. +Explicit builds only launch when `--run` is supplied. Use `--build-only` to +compile with prepared dependencies, or `--check` for read-only prerequisite +diagnostics. Windows keeps each built example beside its runtime DLLs. -For Android, set the SDK/NDK paths and ABI, then run -`scripts/build_android.sh --build-only`. It builds a signed debug APK without -installing it. See [Android instructions](examples/android_touch/README.md). +Choose Android in the menu for SDK/NDK detection and connected-device selection. +Install the Android development tools first; the runner can ask for their paths +but does not download the SDK. It builds, installs and opens a debug APK after +confirmation. The guided Android flow requires Bash and the toolchain described +in [Android instructions](examples/android_touch/README.md). Those instructions +also cover explicit builds without installing on a device. The pinned ImGui commit includes the external-example host options. Use this revision rather than an older installed module. diff --git a/docs/installing-releases.md b/docs/installing-releases.md index 964190b..3d5e858 100644 --- a/docs/installing-releases.md +++ b/docs/installing-releases.md @@ -9,16 +9,18 @@ is required. ## Desktop -Choose a ZIP for your OS and **x64** processor: +Choose a ZIP for your OS and **x64** processor. **Docking** is the recommended download: - **Windows:** Windows 10 or 11. Extract the entire ZIP to a writable folder. - Open `widget_gallery.exe`, `implot_dashboard.exe`, or `glfw_vulkan.exe`. + Open `examples.exe`, then choose an example from its menu. Press Enter for the + Widget gallery. You can also open each example executable directly. Keep the DLLs beside the executables. - **Linux:** Ubuntu 24.04 or a compatible newer distribution, with an X11 or XWayland desktop. Extract the entire ZIP. Run `./run.sh`, or - `./run.sh implot_dashboard` / `./run.sh glfw_vulkan` from the extracted folder. + choose an example from its menu (Enter opens Widget gallery), or run + `./run.sh implot_dashboard` / `./run.sh glfw_vulkan` directly. Keep the `lib` directory beside the executables. If your archive tool drops - executable permissions, run `chmod +x run.sh glfw_vulkan widget_gallery implot_dashboard`. + executable permissions, run `chmod +x run.sh examples glfw_vulkan widget_gallery implot_dashboard`. **Docking** includes ImGui docking and optional platform viewports. **Standard** uses independent ImGui windows. Both contain all three examples. diff --git a/examples/android_touch/README.md b/examples/android_touch/README.md index fce26fb..4ef5f47 100644 --- a/examples/android_touch/README.md +++ b/examples/android_touch/README.md @@ -7,6 +7,14 @@ bridge, and FreeType font. It has its own package ID, Install a JDK, CMake/Ninja, V, Android SDK platform/build-tools 36, and NDK r27c. Android API 24 and a Vulkan-capable device are required. From the repository root: +Run `v run setup.vsh` and choose **Android touch and text** for guided SDK/NDK +detection, device selection, building and installation. Enable USB debugging on +the device and accept its authorization prompt. The runner asks before building +and installing; it never uninstalls an existing app to resolve a signing conflict. +If multiple NDKs or devices are available, it asks which one to use. + +For a build without prompts or device installation: + ```sh export ANDROID_SDK_ROOT=/path/to/android-sdk export ANDROID_NDK_HOME="$ANDROID_SDK_ROOT/ndk/27.3.13750724" diff --git a/packaging/README-linux.txt b/packaging/README-linux.txt index 415b4b0..f66c317 100644 --- a/packaging/README-linux.txt +++ b/packaging/README-linux.txt @@ -1,6 +1,10 @@ V ImGui examples — Ubuntu 24.04 or newer, x86_64 Extract the whole archive. From its directory, run: + ./run.sh + +Choose an example from the menu. Press Enter for Widget gallery. +To launch directly: ./run.sh widget_gallery ./run.sh implot_dashboard ./run.sh glfw_vulkan diff --git a/packaging/README-windows.txt b/packaging/README-windows.txt index e0d0d75..f460942 100644 --- a/packaging/README-windows.txt +++ b/packaging/README-windows.txt @@ -1,6 +1,10 @@ V ImGui examples — Windows 10/11, x64 Extract the whole archive and open: + examples.exe + +Choose an example from the menu. Press Enter for Widget gallery. +You can also open an example directly: widget_gallery.exe implot_dashboard.exe glfw_vulkan.exe diff --git a/packaging/examples.v b/packaging/examples.v new file mode 100644 index 0000000..15a0de1 --- /dev/null +++ b/packaging/examples.v @@ -0,0 +1,50 @@ +module main + +import os + +fn main() { + choices := ['widget_gallery', 'implot_dashboard', 'glfw_vulkan'] + mut example := if os.args.len > 1 { os.args[1] } else { '' } + if example in ['--help', '-h'] { + println('Usage: examples [widget_gallery|implot_dashboard|glfw_vulkan] [example arguments]') + return + } + if example == '' { + println('V ImGui examples\n\n1. Widget gallery — controls, accessibility and virtual lists (default)\n2. ImPlot dashboard — interactive plots and rolling history\n3. GLFW/Vulkan — windows, docking and backend demonstration\nQ. Quit\n') + answer := os.input_opt('Choose an example [1]: ') or { return } + match answer.trim_space().to_lower() { + '', '1' { example = choices[0] } + '2' { example = choices[1] } + '3' { example = choices[2] } + 'q' { return } + else { + eprintln('Choose 1, 2, 3 or Q.') + exit(2) + } + } + } + if example !in choices { + eprintln('Unknown example: ${example}. Use --help for choices.') + exit(2) + } + mut filename := example + $if windows { + filename += '.exe' + } + path := os.join_path(os.dir(os.executable()), filename) + if !os.is_file(path) { + eprintln('Could not find ${filename}. Extract the entire release archive and keep its files together.') + exit(1) + } + mut child := os.new_process(path) + child.set_args(if os.args.len > 2 { os.args[2..] } else { []string{} }) + child.run() + child.wait() + code := child.code + child.close() + if code != 0 { + eprintln('${example} could not run (exit ${code}). Check that the whole archive is extracted and your graphics driver supports Vulkan.') + if os.is_atty(0) != 0 { os.input('Press Enter to close. ') } + } + exit(code) +} diff --git a/packaging/run.sh b/packaging/run.sh index 97d4d89..6c663ca 100755 --- a/packaging/run.sh +++ b/packaging/run.sh @@ -1,7 +1,8 @@ #!/usr/bin/env bash set -euo pipefail package_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) -example=${1:-widget_gallery} +if [[ $# -eq 0 ]]; then exec "$package_dir/examples"; fi +example=$1 case "$example" in glfw_vulkan|widget_gallery|implot_dashboard) ;; *) echo 'Usage: run.sh [glfw_vulkan|widget_gallery|implot_dashboard]' >&2; exit 2 ;; esac if [[ $# -gt 0 ]]; then shift; fi exec "$package_dir/$example" "$@" diff --git a/scripts/release_tools/package.v b/scripts/release_tools/package.v index 95445e5..1d1010b 100644 --- a/scripts/release_tools/package.v +++ b/scripts/release_tools/package.v @@ -61,15 +61,17 @@ fn package_linux(root string, binaries string, imgui string, output string) ![]s library_dir := os.join_path(output, 'lib') os.mkdir_all(library_dir)! mut queue := []Library{} - for example in examples { + mut programs := examples.clone() + programs << 'examples' + for example in programs { source := os.join_path(binaries, example) copy(source, os.join_path(output, example))! os.chmod(os.join_path(output, example), 0o755)! queue << linux_dependencies(source)! } // GLFW loads extension libraries and Vulkan presentation with dlopen. - for name in ['libvulkan.so.1', 'libXrandr.so.2', 'libXinerama.so.1', 'libXcursor.so.1', - 'libXi.so.6', 'libXxf86vm.so.1'] { + for name in ['libvulkan.so.1', 'libXrandr.so.2', 'libXinerama.so.1', 'libXcursor.so.1', 'libXi.so.6', + 'libXxf86vm.so.1'] { queue << Library{ name: name, path: os.join_path('/usr/lib/x86_64-linux-gnu', name) } } mut digests := map[string]string{} @@ -94,7 +96,7 @@ fn package_linux(root string, binaries string, imgui string, output string) ![]s os.chmod(os.join_path(output, 'run.sh'), 0o755)! copy(os.join_path(root, 'packaging', 'README-linux.txt'), os.join_path(output, 'README.txt'))! mut audit_files := []string{} - for example in examples { + for example in programs { file := os.join_path(output, example) command('patchelf', ['--set-rpath', r'$ORIGIN/lib', file])! audit_files << file @@ -150,6 +152,7 @@ fn windows_os_library(name string) bool { 'netapi32.dll', 'mswsock.dll', 'normaliz.dll', + 'dbghelp.dll', 'd3d11.dll', 'dxgi.dll', 'opengl32.dll', @@ -229,6 +232,15 @@ pub fn package_release(root string, binaries string, imgui string, destination s } os.mkdir_all(os.join_path(destination, 'licenses'))! output := os.real_path(destination) + mut launcher := os.join_path(binaries, 'examples') + $if windows { + launcher += '.exe' + } + command(@VEXE, ['-prod', '-o', launcher, os.join_path(root, 'packaging', 'examples.v')])! + copy(launcher, os.join_path(output, os.file_name(launcher)))! + $if !windows { + os.chmod(os.join_path(output, 'examples'), 0o755)! + } mut runtime := []string{} $if windows { runtime = package_windows(root, binaries, imgui, output)! diff --git a/scripts/smoke_desktop.sh b/scripts/smoke_desktop.sh index 4730ed0..9a0995e 100755 --- a/scripts/smoke_desktop.sh +++ b/scripts/smoke_desktop.sh @@ -12,7 +12,9 @@ for example in glfw_vulkan widget_gallery implot_dashboard; do frames=60 # Exercise the ring buffer after wrapping past its 600-sample capacity. if [[ "$example" == implot_dashboard ]]; then frames=660; fi - VIMGUI_SMOKE_FRAMES="$frames" timeout 60s xvfb-run -a "$binary_dir/$example" > "$log_dir/$example.log" 2>&1 + command=("$binary_dir/$example") + if [[ -x "$binary_dir/examples" ]]; then command=("$binary_dir/examples" "$example"); fi + VIMGUI_SMOKE_FRAMES="$frames" timeout 60s xvfb-run -a "${command[@]}" > "$log_dir/$example.log" 2>&1 cat "$log_dir/$example.log" if rg -iq 'VUID-|validation error|segmentation|assert|fatal' "$log_dir/$example.log"; then echo "Rendering failed: $example" >&2 diff --git a/setup.vsh b/setup.vsh index 230f87c..2c02c87 100644 --- a/setup.vsh +++ b/setup.vsh @@ -1,34 +1,157 @@ #!/usr/bin/env -S v run -// Installs/builds antono2.imgui and compiles this graphical example. It does -// not launch a window; run the example normally after setup to see it. +// Guided setup, build and run; explicit flags also support automation. import os fn run(command string) ! { println('\n> ${command}') - result := os.execute(command) - if result.output.trim_space() != '' { - println(result.output.trim_right('\r\n')) + code := os.system(command) + if code != 0 { + return error('command failed with exit code ${code}; see the output above') } +} + +fn ask(prompt string) !string { + return (os.input_opt(prompt) or { return error('Input closed. Use explicit flags for noninteractive runs.') }).trim_space() +} + +fn confirm(prompt string) !bool { + return (ask(prompt + ' [y/N]: ')!).to_lower() in ['y', 'yes'] +} + +fn android_environment(interactive bool) ! { + mut sdk := os.getenv('ANDROID_SDK_ROOT') + if sdk == '' { + sdk = os.getenv('ANDROID_HOME') + } + if sdk == '' { + for candidate in [os.join_path(os.home_dir(), 'Android', 'Sdk'), + os.join_path(os.home_dir(), 'Library', 'Android', 'sdk'), + os.join_path(os.getenv('LOCALAPPDATA'), 'Android', 'Sdk')] { + if os.is_dir(candidate) { + sdk = candidate + break + } + } + } + if sdk == '' && interactive { + sdk = ask('Android SDK directory: ')! + } + if !os.is_dir(sdk) { + return error('Android SDK not found. Set ANDROID_SDK_ROOT to an installed SDK; see examples/android_touch/README.md.') + } + if !os.is_file(os.join_path(sdk, 'platforms', 'android-36', 'android.jar')) || !os.is_dir(os.join_path(sdk, 'build-tools', '36.0.0')) { + return error('Install Android SDK Platform 36 and Build-Tools 36.0.0 using the SDK Manager, then retry.') + } + os.setenv('ANDROID_SDK_ROOT', sdk, true) + mut ndk := os.getenv('ANDROID_NDK_HOME') + if ndk == '' { + ndk = os.getenv('ANDROID_NDK_ROOT') + } + if ndk == '' { + ndk_directory := os.join_path(sdk, 'ndk') + if os.is_dir(ndk_directory) { + mut versions := os.ls(ndk_directory)! + versions = versions.filter(os.is_file(os.join_path(ndk_directory, it, 'build', 'cmake', 'android.toolchain.cmake'))) + versions.sort() + if versions.len == 1 { + ndk = os.join_path(ndk_directory, versions[0]) + } else if versions.len > 1 && interactive { + println('Installed NDKs:') + for i, version in versions { + println('${i + 1}. ${version}') + } + choice := ask('Choose an NDK number: ')! + if choice.int() < 1 || choice.int() > versions.len { + return error('Invalid NDK choice') + } + ndk = os.join_path(ndk_directory, versions[choice.int() - 1]) + } + } + } + if ndk == '' && interactive { + ndk = ask('Android NDK directory: ')! + } + if !os.is_file(os.join_path(ndk, 'build', 'cmake', 'android.toolchain.cmake')) { + return error('Android NDK not found. Set ANDROID_NDK_HOME to an installed NDK.') + } + os.setenv('ANDROID_NDK_HOME', ndk, true) + os.setenv('PATH', os.join_path(sdk, 'platform-tools') + os.path_delimiter + os.getenv('PATH'), true) +} + +fn android_device(interactive bool) ! { + if !os.exists_in_system_path('adb') { + return error('Install Android SDK Platform-Tools before installing on a device.') + } + result := os.execute('adb devices') if result.exit_code != 0 { - return error('command failed with exit code ${result.exit_code}') + return error('Could not list Android devices: ${result.output}') + } + mut devices := []string{} + for line in result.output.split_into_lines() { + parts := line.fields() + if parts.len == 2 && parts[1] == 'device' { devices << parts[0] } + } + if devices.len == 0 { + return error('No authorized device. Connect it, enable USB debugging and accept its authorization prompt, then retry.') + } + mut serial := os.getenv('ANDROID_SERIAL') + if serial != '' && serial !in devices { + return error('ANDROID_SERIAL is not an authorized connected device.') + } + if serial == '' && devices.len == 1 { + serial = devices[0] + } + if serial == '' && interactive { + for i, device in devices { + println('${i + 1}. ${device}') + } + choice := ask('Choose a device number: ')! + if choice.int() < 1 || choice.int() > devices.len { + return error('Invalid device choice') + } + serial = devices[choice.int() - 1] + } + if serial == '' { + return error('Several devices are connected. Set ANDROID_SERIAL to choose one.') } + os.setenv('ANDROID_SERIAL', serial, true) + println('Device: ${serial}') } -fn main() { +fn guided_main() ! { project_dir := os.dir(os.real_path(@FILE)) + // Keep upstream setup and compilation on the same V compiler as this runner. + os.setenv('PATH', @VEXEROOT + os.path_delimiter + os.getenv('PATH'), true) mut mode := '--install' mut example := 'glfw_vulkan' + interactive := os.args.len == 1 + mut launch := false + if interactive { + println('V ImGui examples\n\n1. Widget gallery — controls, tables and popups (default)\n2. ImPlot dashboard — interactive plots\n3. GLFW/Vulkan — windows and backend demonstration\n4. Android touch and text — device app\nQ. Quit\n') + choice := (ask('Choose an example [1]: ')!).to_lower() + match choice { + '', '1' { example = 'widget_gallery' } + '2' { example = 'implot_dashboard' } + '3' { example = 'glfw_vulkan' } + '4' { example = 'android_touch' } + 'q' { return } + else { return error('Choose 1, 2, 3, 4 or Q.') } + } + launch = true + } mut index := 1 for index < os.args.len { arg := os.args[index] if arg in ['--install', '--check', '--build-only'] { mode = arg + } else if arg == '--run' { + launch = true } else if arg == '--example' && index + 1 < os.args.len { index++ example = os.args[index] } else if arg in ['-h', '--help'] { - println('Usage: v run setup.vsh [--install|--check|--build-only] [--example glfw_vulkan|widget_gallery|implot_dashboard]') + println('Usage: v run setup.vsh [--install|--check|--build-only] [--run] [--example glfw_vulkan|widget_gallery|implot_dashboard|android_touch]\nNo arguments: guided setup, build and launch. Explicit flags skip menus; system installers may request administrator authorization. --check makes no changes.') return } else { eprintln('Unknown or incomplete option: ${arg}') @@ -36,9 +159,11 @@ fn main() { } index++ } - if example !in ['glfw_vulkan', 'widget_gallery', 'implot_dashboard'] { - eprintln('Unknown desktop example: ${example}. For Android, use scripts/build_android.sh.') - exit(2) + if example !in ['glfw_vulkan', 'widget_gallery', 'implot_dashboard', 'android_touch'] { + return error('Unknown example: ${example}. Use --help for choices.') + } + if mode == '--check' && launch { + return error('--check cannot be combined with --run.') } source := if example == 'glfw_vulkan' { project_dir @@ -52,38 +177,98 @@ fn main() { } os.setenv('VMODULES', module_dir, true) imgui_root := os.join_path(module_dir, 'antono2', 'imgui') - revision := (os.read_file(os.join_path(project_dir, 'IMGUI_REVISION')) or { panic(err) }).trim_space() + revision := (os.read_file(os.join_path(project_dir, 'IMGUI_REVISION')) or { return err }).trim_space() if revision.len != 40 || !revision.bytes().all((it >= `0` && it <= `9`) || (it >= `a` && it <= `f`)) { - panic('IMGUI_REVISION must contain a full lowercase Git commit SHA') + return error('IMGUI_REVISION must contain a full lowercase Git commit SHA') + } + mut example_binary := os.join_path(project_dir, 'build', example) + $if windows { + example_binary = os.join_path(imgui_root, 'build', 'windows-demo', example + '.exe') + } + if interactive && example != 'android_touch' && os.is_file(example_binary) { + choice := (ask('Existing build: run [Enter], rebuild [b], prepare dependencies again [s], or quit [q]: ')!).to_lower() + match choice { + '' { + run(os.quoted_path(example_binary))! + return + } + 'b' { mode = '--build-only' } + 's' {} + 'q' { return } + else { return error('Choose Enter, b, s or q.') } + } + } + if example == 'android_touch' { + android_environment(interactive)! + for tool in ['bash', 'git', 'cmake', 'ninja', 'java', 'javac'] { + if !os.exists_in_system_path(tool) { + return error('Missing ${tool}. Install it and retry; see examples/android_touch/README.md.') + } + } + if launch { android_device(interactive)! } + if mode == '--check' { + println('Android tools and SDK/NDK paths are available.') + return + } + if !launch && os.getenv('ANDROID_ABI') == '' { + return error('Set ANDROID_ABI to armeabi-v7a, arm64-v8a or x86_64 for a build without a device.') + } + if interactive && !confirm('Build a debug APK and install/open it on this device? Dependencies may be downloaded; an installed app with a different signing key must be removed manually.')! { + return + } + os.setenv('IMGUI_DIR', imgui_root, true) + os.setenv('VIMGUI_ANDROID_APPLICATION_UI', '1', true) + run('bash ${os.quoted_path(os.join_path(project_dir, 'scripts', 'build_android.sh'))} ${if launch { + 'run' + } else { + '--build-only' + }}')! + return + } + if !os.exists_in_system_path('git') { + return error('Git is required. Install Git, then run setup again.') + } + if interactive && mode == '--install' { + println('Prerequisites: V and Git are available.') + if !os.exists_in_system_path('cmake') { + println('CMake is missing. The upstream setup will install it on supported systems or show installation instructions.') + } + println('Setup will download pinned dependencies into ${module_dir} and build ${example}.\nThe upstream setup may install system packages (and request administrator access). Compilation can require about 11 GiB of RAM or swap.') + if !confirm('Continue with setup, build and launch?')! { + return + } + } + if mode == '--check' && !os.is_file(os.join_path(imgui_root, 'v.mod')) { + return error('Pinned dependencies are not prepared. Run the guided setup or --install first; no files were changed.') } if mode == '--install' { - os.mkdir_all(os.dir(imgui_root)) or { panic(err) } + os.mkdir_all(os.dir(imgui_root)) or { return err } if !os.is_file(os.join_path(imgui_root, 'v.mod')) { - run('git clone https://github.com/antono2/imgui.git ${os.quoted_path(imgui_root)}') or { panic(err) } + run('git clone https://github.com/antono2/imgui.git ${os.quoted_path(imgui_root)}') or { return err } } // Never discard changes in an existing dependency checkout. status := os.execute('git -C ${os.quoted_path(imgui_root)} status --porcelain') if status.exit_code != 0 || status.output.trim_space() != '' { - panic('ImGui dependency checkout contains changes; commit them or use a fresh VMODULES directory') + return error('ImGui dependency checkout contains changes; commit them or use a fresh VMODULES directory') } - run('git -C ${os.quoted_path(imgui_root)} fetch origin ${revision.trim_space()}') or { panic(err) } - run('git -C ${os.quoted_path(imgui_root)} checkout --detach ${revision.trim_space()}') or { panic(err) } + run('git -C ${os.quoted_path(imgui_root)} fetch origin ${revision.trim_space()}') or { return err } + run('git -C ${os.quoted_path(imgui_root)} checkout --detach ${revision.trim_space()}') or { return err } } current := os.execute('git -C ${os.quoted_path(imgui_root)} rev-parse HEAD') if current.exit_code != 0 || current.output.trim_space() != revision.trim_space() { - panic('ImGui must match IMGUI_REVISION; run --install with a clean dependency checkout') + return error('ImGui must match IMGUI_REVISION; run --install with a clean dependency checkout') } imgui_setup := os.join_path(imgui_root, 'setup.vsh') if !os.is_file(imgui_setup) { - panic('Run setup.vsh --install first to prepare the pinned ImGui dependency') + return error('Run setup.vsh --install first to prepare the pinned ImGui dependency') } if mode != '--build-only' { - run('v run ${os.quoted_path(imgui_setup)} ${mode}') or { panic(err) } + run('v run ${os.quoted_path(imgui_setup)} ${mode}') or { return err } } if mode == '--check' { return } - os.mkdir_all(os.join_path(project_dir, 'build')) or { panic(err) } + os.mkdir_all(os.join_path(project_dir, 'build')) or { return err } $if windows { vulkan_sdk := os.execute('powershell -NoProfile -Command "[Environment]::GetEnvironmentVariable(\'VULKAN_SDK\', \'Machine\')"') @@ -92,8 +277,9 @@ fn main() { } runner := os.join_path(imgui_root, 'scripts', 'run_demo_windows.ps1') run('powershell -NoProfile -ExecutionPolicy Bypass -File ${os.quoted_path(runner)} -BuildOnly -DemoDirectory ${os.quoted_path(project_dir)} -DemoSource ${os.quoted_path(source)}') or { - panic(err) + return err } + os.cp(os.join_path(imgui_root, 'build', 'windows-demo', 'v_imgui_demo.exe'), example_binary)! } $else { $if linux { os.setenv('VULKAN_SDK', '/usr', false) @@ -108,11 +294,24 @@ fn main() { os.setenv('GLFW_INCLUDE', os.join_path(prefix, 'include'), true) os.setenv('GLFW_LIB', os.join_path(prefix, 'lib'), true) } - executable := os.join_path(project_dir, 'build', example) module_path := '${module_dir}/antono2|@vlib|@vmodules' - run('v -no-memory-limit -path ${os.quoted_path(module_path)} -o ${os.quoted_path(executable)} ${os.quoted_path(source)}') or { - panic(err) + staged_binary := example_binary + '.building' + if os.exists(staged_binary) { os.rm(staged_binary)! } + run('v -no-memory-limit -path ${os.quoted_path(module_path)} -o ${os.quoted_path(staged_binary)} ${os.quoted_path(source)}') or { + return err } + if !os.is_file(staged_binary) { + return error('The compiler did not produce an executable. Review its output above.') + } + os.mv(staged_binary, example_binary)! + } + println('\nBuilt ${example_binary}') + if launch { run(os.quoted_path(example_binary))! } +} + +fn main() { + guided_main() or { + eprintln('\n${err}') + exit(1) } - println('\nBuilt ${example}. On Linux/macOS, run build/${example}; on Windows, run the executable reported above.') } From eed34734ae2dc132bdf5fe19a6376b25008702e3 Mon Sep 17 00:00:00 2001 From: antono2 Date: Sat, 3 Oct 2026 07:49:12 +0200 Subject: [PATCH 10/29] Make setup and release scripts directly executable --- .github/workflows/compile.yml | 1 + .github/workflows/release-packages.yml | 8 ++++---- IMGUI_REVISION | 2 +- README.md | 10 ++++++---- docs/releasing.md | 12 +++++++----- examples/android_touch/README.md | 2 +- examples/glfw_vulkan/README.md | 2 +- examples/implot_dashboard/README.md | 2 +- examples/widget_gallery/README.md | 4 ++-- scripts/build_release.v | 4 +++- scripts/collect_licenses.v | 4 +++- scripts/package_release.v | 4 +++- scripts/sign_android_release.v | 2 ++ setup.vsh | 2 +- 14 files changed, 36 insertions(+), 23 deletions(-) mode change 100644 => 100755 scripts/build_release.v mode change 100644 => 100755 scripts/collect_licenses.v mode change 100644 => 100755 scripts/package_release.v mode change 100644 => 100755 scripts/sign_android_release.v mode change 100644 => 100755 setup.vsh diff --git a/.github/workflows/compile.yml b/.github/workflows/compile.yml index ffee0cf..c186c16 100644 --- a/.github/workflows/compile.yml +++ b/.github/workflows/compile.yml @@ -67,6 +67,7 @@ jobs: - name: Compile GLFW/Vulkan example run: | v -check-syntax source/demo/setup.vsh + ./source/demo/setup.vsh --help for example in glfw_vulkan widget_gallery implot_dashboard; do source=source/demo if [ "$example" != glfw_vulkan ]; then source="source/demo/examples/$example"; fi diff --git a/.github/workflows/release-packages.yml b/.github/workflows/release-packages.yml index 2d8f2c4..46f3607 100644 --- a/.github/workflows/release-packages.yml +++ b/.github/workflows/release-packages.yml @@ -14,9 +14,9 @@ jobs: variant: [docking, standard] include: - variant: docking - imgui-ref: 047f2d94dbde25fc314fb3e0ed8fd07243d6c7f6 + imgui-ref: f13c1c8e6a79e81df9867155c589e5eeafa17b37 - variant: standard - imgui-ref: 80fda7761bb3dd06b1ef5512297a5e51f74805d9 + imgui-ref: d66ee51490378ab651f1f184cff9eff0949dc20a runs-on: ${{ matrix.os }} timeout-minutes: 45 env: @@ -62,7 +62,7 @@ jobs: Copy-Item build/volk/volk.h, build/volk/volk.c $Destination - name: Build, bundle and render Linux releases if: runner.os == 'Linux' - run: v -prod run scripts/build_release.v '${{ matrix.variant }}' + run: ./scripts/build_release.v '${{ matrix.variant }}' - name: Exercise bundled gallery through native AT-SPI if: runner.os == 'Linux' run: | @@ -123,7 +123,7 @@ jobs: - name: Build accessible touch app run: bash scripts/build_android.sh --build-only - name: Collect Android notices - run: v run scripts/collect_licenses.v build/modules/antono2/imgui "build/android-touch-$ANDROID_ABI/licenses" android + run: ./scripts/collect_licenses.v build/modules/antono2/imgui "build/android-touch-$ANDROID_ABI/licenses" android - name: Upload unsigned APK and notices for release signing uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 with: diff --git a/IMGUI_REVISION b/IMGUI_REVISION index 59d2ba4..0246766 100644 --- a/IMGUI_REVISION +++ b/IMGUI_REVISION @@ -1 +1 @@ -047f2d94dbde25fc314fb3e0ed8fd07243d6c7f6 +f13c1c8e6a79e81df9867155c589e5eeafa17b37 diff --git a/README.md b/README.md index c3e826f..7fd5a3a 100644 --- a/README.md +++ b/README.md @@ -37,11 +37,13 @@ want the Standard variant. Install V and Git, then run this command from the checkout: ```sh -v run setup.vsh +./setup.vsh ``` Use the official V compiler. The tested compiler revisions are recorded in [the CI compiler setup](.github/actions/setup-v/action.yml). +On Windows, use `v run setup.vsh` with the same options; the direct executable +script commands below apply to Linux and macOS. The guided runner lets you choose an example, explains setup changes, builds it and opens it. It streams build progress and offers to run an existing desktop @@ -53,9 +55,9 @@ For automation, explicit options skip the menus and setup confirmation. System installers may still request administrator authorization: ```sh -v run setup.vsh --install --example widget_gallery --run -v run setup.vsh --build-only --example implot_dashboard --run -v run setup.vsh --check --example widget_gallery +./setup.vsh --install --example widget_gallery --run +./setup.vsh --build-only --example implot_dashboard --run +./setup.vsh --check --example widget_gallery ``` Setup uses an isolated `build/modules` dependency directory and checks out the diff --git a/docs/releasing.md b/docs/releasing.md index 80c7a2c..cc9aaec 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -9,18 +9,20 @@ APKs as release updates: their signing keys are temporary. The release helpers are V programs with shared build, packaging, license and ZIP code in `scripts/release_tools/`. From prepared dependencies, run -`v -prod run scripts/build_release.v docking` (or `standard`). On Windows first -load `. scripts/setup_windows_build.ps1` in PowerShell to select Visual Studio. -To package existing binaries, use `v -prod run scripts/package_release.v +`./scripts/build_release.v docking` (or `standard`) on Linux. On Windows +first load `. scripts/setup_windows_build.ps1` in PowerShell to select Visual +Studio, then use `v -prod run scripts/build_release.v docking`. For the other V +helpers below, Windows also uses `v run` (or `v -prod run`) before the script path. +To package existing binaries, use `./scripts/package_release.v binary-dir imgui-dir output-dir docking`. License collection uses -`v run scripts/collect_licenses.v imgui-dir output-dir`, with `android` as its +`./scripts/collect_licenses.v imgui-dir output-dir`, with `android` as its last argument for mobile notices. Desktop ZIPs are ready to promote after reviewing the checks. Android APKs are signed outside CI with the persistent private release key: ```sh -v -prod run scripts/sign_android_release.v \ +./scripts/sign_android_release.v \ --input /path/to/android-release-input-armeabi-v7a \ --input /path/to/android-release-input-arm64-v8a \ --input /path/to/android-release-input-x86_64 \ diff --git a/examples/android_touch/README.md b/examples/android_touch/README.md index 4ef5f47..eaf6a1d 100644 --- a/examples/android_touch/README.md +++ b/examples/android_touch/README.md @@ -7,7 +7,7 @@ bridge, and FreeType font. It has its own package ID, Install a JDK, CMake/Ninja, V, Android SDK platform/build-tools 36, and NDK r27c. Android API 24 and a Vulkan-capable device are required. From the repository root: -Run `v run setup.vsh` and choose **Android touch and text** for guided SDK/NDK +Run `./setup.vsh` and choose **Android touch and text** for guided SDK/NDK detection, device selection, building and installation. Enable USB debugging on the device and accept its authorization prompt. The runner asks before building and installing; it never uninstalls an existing app to resolve a signing conflict. diff --git a/examples/glfw_vulkan/README.md b/examples/glfw_vulkan/README.md index d174730..60b8f48 100644 --- a/examples/glfw_vulkan/README.md +++ b/examples/glfw_vulkan/README.md @@ -3,7 +3,7 @@ From the repository root: ```sh -v run setup.vsh +./setup.vsh --example glfw_vulkan ``` On Linux/macOS, launch `build/glfw_vulkan`. Windows setup reports its executable. diff --git a/examples/implot_dashboard/README.md b/examples/implot_dashboard/README.md index 9c67452..6ed3d7d 100644 --- a/examples/implot_dashboard/README.md +++ b/examples/implot_dashboard/README.md @@ -3,7 +3,7 @@ From the repository root: ```sh -v run setup.vsh --example implot_dashboard +./setup.vsh --example implot_dashboard ``` On Linux/macOS, launch `build/implot_dashboard`. Windows setup reports its executable. diff --git a/examples/widget_gallery/README.md b/examples/widget_gallery/README.md index 8cb3e9d..1062ede 100644 --- a/examples/widget_gallery/README.md +++ b/examples/widget_gallery/README.md @@ -3,7 +3,7 @@ From the repository root: ```sh -v run setup.vsh --example widget_gallery +./setup.vsh --example widget_gallery ``` On Linux/macOS, launch `build/widget_gallery`. Windows setup reports its executable. @@ -29,6 +29,6 @@ status/progress, and a virtual file list. Focus last file reveals the list and moves keyboard/native focus to its last row. Switch to Raw ImGui widgets for the original tables, popups and secondary windows. See [release installation](../../docs/installing-releases.md) for supported downloads. -Use `v -prod run scripts/build_release.v docking` to build the accessible +Use `./scripts/build_release.v docking` to build the accessible package from prepared dependencies (Windows uses a Visual Studio developer shell). The normal setup command remains the direct raw API build. diff --git a/scripts/build_release.v b/scripts/build_release.v old mode 100644 new mode 100755 index 508cfe6..b2b316a --- a/scripts/build_release.v +++ b/scripts/build_release.v @@ -1,3 +1,5 @@ +#!/usr/bin/env -S v -prod run + module main import os @@ -23,7 +25,7 @@ fn main() { fn run() ! { if os.args.len != 2 || os.args[1] !in ['docking', 'standard'] { - return error('Usage: v run scripts/build_release.v docking|standard') + return error('Usage: ./scripts/build_release.v docking|standard') } variant := os.args[1] root := os.dir(os.dir(@FILE)) diff --git a/scripts/collect_licenses.v b/scripts/collect_licenses.v old mode 100644 new mode 100755 index b8d3d90..d9b429d --- a/scripts/collect_licenses.v +++ b/scripts/collect_licenses.v @@ -1,3 +1,5 @@ +#!/usr/bin/env -S v run + module main import os @@ -5,7 +7,7 @@ import release_tools fn main() { if os.args.len !in [3, 4] || (os.args.len == 4 && os.args[3] != 'android') { - eprintln('Usage: v run scripts/collect_licenses.v imgui-dir output-dir [android]') + eprintln('Usage: ./scripts/collect_licenses.v imgui-dir output-dir [android]') exit(2) } release_tools.collect_licenses(os.dir(os.dir(@FILE)), os.args[1], os.args[2], os.args.len == 4) or { diff --git a/scripts/package_release.v b/scripts/package_release.v old mode 100644 new mode 100755 index 9c364f2..3dc51b7 --- a/scripts/package_release.v +++ b/scripts/package_release.v @@ -1,3 +1,5 @@ +#!/usr/bin/env -S v -prod run + module main import os @@ -5,7 +7,7 @@ import release_tools fn main() { if os.args.len != 5 { - eprintln('Usage: v run scripts/package_release.v binary-dir imgui-dir output-dir docking|standard') + eprintln('Usage: ./scripts/package_release.v binary-dir imgui-dir output-dir docking|standard') exit(2) } release_tools.package_release(os.dir(os.dir(@FILE)), os.args[1], os.args[2], os.args[3], os.args[4]) or { diff --git a/scripts/sign_android_release.v b/scripts/sign_android_release.v old mode 100644 new mode 100755 index 3ee30ea..38f3bbd --- a/scripts/sign_android_release.v +++ b/scripts/sign_android_release.v @@ -1,3 +1,5 @@ +#!/usr/bin/env -S v -prod run + // Combine trusted unsigned CI APKs and sign with the private release identity. module main diff --git a/setup.vsh b/setup.vsh old mode 100644 new mode 100755 index 2c02c87..f327794 --- a/setup.vsh +++ b/setup.vsh @@ -151,7 +151,7 @@ fn guided_main() ! { index++ example = os.args[index] } else if arg in ['-h', '--help'] { - println('Usage: v run setup.vsh [--install|--check|--build-only] [--run] [--example glfw_vulkan|widget_gallery|implot_dashboard|android_touch]\nNo arguments: guided setup, build and launch. Explicit flags skip menus; system installers may request administrator authorization. --check makes no changes.') + println('Usage: ./setup.vsh [--install|--check|--build-only] [--run] [--example glfw_vulkan|widget_gallery|implot_dashboard|android_touch]\nOn Windows: v run setup.vsh with the same options.\nNo arguments: guided setup, build and launch. Explicit flags skip menus; system installers may request administrator authorization. --check makes no changes.') return } else { eprintln('Unknown or incomplete option: ${arg}') From f276e56f7a0726e07c96a82f9d25466e0cd7eb29 Mon Sep 17 00:00:00 2001 From: antono2 Date: Sat, 3 Oct 2026 08:09:56 +0200 Subject: [PATCH 11/29] Add shared Escape and borderless fullscreen shortcuts --- .github/workflows/compile.yml | 4 +- .github/workflows/release-packages.yml | 5 +- README.md | 4 + docs/installing-releases.md | 2 + docs/testing.md | 6 ++ examples/glfw_vulkan/README.md | 5 ++ examples/glfw_vulkan/imgui_bridge.h | 20 +++++ examples/glfw_vulkan/main.c.v | 22 ++++- examples/glfw_vulkan/shortcuts.v | 78 ++++++++++++++++ packaging/README-linux.txt | 3 + packaging/README-windows.txt | 3 + tests/desktop/shortcuts.sh | 118 +++++++++++++++++++++++++ 12 files changed, 267 insertions(+), 3 deletions(-) create mode 100644 examples/glfw_vulkan/shortcuts.v create mode 100755 tests/desktop/shortcuts.sh diff --git a/.github/workflows/compile.yml b/.github/workflows/compile.yml index c186c16..8f156ab 100644 --- a/.github/workflows/compile.yml +++ b/.github/workflows/compile.yml @@ -60,7 +60,7 @@ jobs: make local=1 echo "$GITHUB_WORKSPACE/toolchain/v" >> "$GITHUB_PATH" - name: Install native dependencies - run: sudo apt-get update && sudo apt-get install -y build-essential cmake libglfw3-dev libvulkan-dev libvulkan-volk-dev pkg-config mesa-vulkan-drivers vulkan-validationlayers xvfb + run: sudo apt-get update && sudo apt-get install -y build-essential cmake libglfw3-dev libvulkan-dev libvulkan-volk-dev pkg-config mesa-vulkan-drivers vulkan-validationlayers xvfb openbox xdotool x11-utils wmctrl - name: Build native ImGui library working-directory: source/modules/antono2/imgui run: v run build_vimgui.vsh --linkage shared --glfw system @@ -75,6 +75,8 @@ jobs: done - name: Render desktop examples with Vulkan validation run: bash source/demo/scripts/smoke_desktop.sh "$RUNNER_TEMP" + - name: Check desktop keyboard shortcuts + run: ./source/demo/tests/desktop/shortcuts.sh "$RUNNER_TEMP" --raw-gallery - name: Verify executable run: test -x "$RUNNER_TEMP/glfw_vulkan" && test -x "$RUNNER_TEMP/widget_gallery" && test -x "$RUNNER_TEMP/implot_dashboard" diff --git a/.github/workflows/release-packages.yml b/.github/workflows/release-packages.yml index 46f3607..5aa3b84 100644 --- a/.github/workflows/release-packages.yml +++ b/.github/workflows/release-packages.yml @@ -42,7 +42,7 @@ jobs: path: build/modules/antono2/glfw - name: Install Linux build and rendering tools if: runner.os == 'Linux' - run: sudo apt-get update && sudo apt-get install -y build-essential cmake libglfw3-dev libvulkan-dev libvulkan-volk-dev libdbus-1-dev libatk1.0-dev libatk-bridge2.0-dev libatspi2.0-dev libglib2.0-dev at-spi2-core dbus-x11 pkg-config mesa-vulkan-drivers vulkan-validationlayers xvfb patchelf zip xdotool + run: sudo apt-get update && sudo apt-get install -y build-essential cmake libglfw3-dev libvulkan-dev libvulkan-volk-dev libdbus-1-dev libatk1.0-dev libatk-bridge2.0-dev libatspi2.0-dev libglib2.0-dev at-spi2-core dbus-x11 pkg-config mesa-vulkan-drivers vulkan-validationlayers xvfb patchelf zip xdotool openbox x11-utils wmctrl - name: Install Windows Vulkan build tools if: runner.os == 'Windows' shell: bash @@ -68,6 +68,9 @@ jobs: run: | c++ -std=c++17 tests/desktop/accessibility.cpp $(pkg-config --cflags --libs atspi-2 gio-2.0) -o build/accessibility-check GSETTINGS_BACKEND=memory dbus-run-session -- xvfb-run -a build/accessibility-check "build/v-imgui-examples-linux-x64-${{ matrix.variant }}/widget_gallery" "build/release-atspi-${{ matrix.variant }}.log" + - name: Check bundled desktop keyboard shortcuts + if: runner.os == 'Linux' + run: ./tests/desktop/shortcuts.sh "build/v-imgui-examples-linux-x64-${{ matrix.variant }}" - name: Build and audit Windows release DLLs if: runner.os == 'Windows' shell: pwsh diff --git a/README.md b/README.md index 7fd5a3a..e2bf3a5 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,10 @@ want the Standard variant. - **Linux:** extract the ZIP and run `./run.sh` for the example menu. - **Android:** install the universal APK and open **V ImGui Touch Examples**. +In each desktop example, **Escape** quits when a control is not using it to +cancel an edit, drag, popup or navigation operation. **F11** toggles borderless +fullscreen on the window's monitor; pressing it again restores the window. + ## Build from source Install V and Git, then run this command from the checkout: diff --git a/docs/installing-releases.md b/docs/installing-releases.md index 3d5e858..ecff7f9 100644 --- a/docs/installing-releases.md +++ b/docs/installing-releases.md @@ -25,6 +25,8 @@ Choose a ZIP for your OS and **x64** processor. **Docking** is the recommended d **Docking** includes ImGui docking and optional platform viewports. **Standard** uses independent ImGui windows. Both contain all three examples. You can move the extracted folder; binaries use package-relative library paths. +Press **F11** to toggle borderless fullscreen and restore the previous window +position and size. **Escape** quits, after any active edit or popup consumes it. `licenses/` and `RUNTIME-LIBRARIES.txt` identify bundled components. ## Android: installation without a Play Store diff --git a/docs/testing.md b/docs/testing.md index dbe1e66..c3d2e17 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -30,6 +30,12 @@ scripts/smoke_desktop.sh "$PWD/build" - [x] Change plot amplitude/frequency and series visibility, including legend clicks. - [x] Pan/zoom waveforms; pause/resume/reset history and observe a full wrap. - [x] Close each application normally. +- [x] Escape quits each desktop example; active text editing and popups consume it first. +- [x] F11 toggles borderless fullscreen, preserves normal/maximized restore bounds, and does not repeat while held. + +Run `./tests/desktop/shortcuts.sh binary-dir --raw-gallery` for keyboard checks +of source builds, or omit `--raw-gallery` for accessible release builds. The +check uses its own Xvfb display with Openbox, xdotool, xprop and wmctrl. ## Android interaction diff --git a/examples/glfw_vulkan/README.md b/examples/glfw_vulkan/README.md index 60b8f48..ef8229d 100644 --- a/examples/glfw_vulkan/README.md +++ b/examples/glfw_vulkan/README.md @@ -9,6 +9,11 @@ From the repository root: On Linux/macOS, launch `build/glfw_vulkan`. Windows setup reports its executable. The root `main.v` still launches this example. +All desktop examples share **Escape** to quit and **F11** to toggle borderless +fullscreen. Escape first cancels an active edit, drag, popup or navigation +operation. Frame callbacks that bind Escape themselves must set +`app.escape_handled = true` in the frame where they consume it. + `main.c.v` owns the GLFW window, Vulkan objects, frame submission, and shutdown. `ui.v` owns the default demo's persistent state. Other desktop examples pass a frame callback and borrowed state pointer to `glfw_vulkan.run`. Optional diff --git a/examples/glfw_vulkan/imgui_bridge.h b/examples/glfw_vulkan/imgui_bridge.h index ff4dcac..9186fdb 100644 --- a/examples/glfw_vulkan/imgui_bridge.h +++ b/examples/glfw_vulkan/imgui_bridge.h @@ -26,4 +26,24 @@ static inline bool v_imgui_example_draw_data_is_minimized(const ImDrawData *draw return draw_data->DisplaySize.x <= 0.0f || draw_data->DisplaySize.y <= 0.0f; } +// Snapshot before NewFrame: Escape may clear an edit or popup during NewFrame. +static inline bool v_imgui_example_escape_reserved(void) { + const ImGuiContext *g = igGetCurrentContext(); + return igIsAnyItemActive() || igGetDragDropPayload() != NULL || + igIsPopupOpen_Str(NULL, ImGuiPopupFlags_AnyPopupId | ImGuiPopupFlags_AnyPopupLevel) || + g->NavLayer != ImGuiNavLayer_Main || g->NavWindowingTarget != NULL || + (g->NavWindow && g->NavWindow != g->NavWindow->RootWindow && + !(g->NavWindow->RootWindowForNav->Flags & ImGuiWindowFlags_Popup) && + g->NavWindow->RootWindowForNav->ParentWindow); +} + +static inline bool v_imgui_example_escape_pressed(void) { + // ImGuiKeyOwner_NoOwner is -1; zero means any owner. + return igIsKeyPressed_InputFlags(ImGuiKey_Escape, 0, (ImGuiID)-1); +} + +static inline bool v_imgui_example_fullscreen_pressed(void) { + return igIsKeyPressed_Bool(ImGuiKey_F11, false); +} + #endif diff --git a/examples/glfw_vulkan/main.c.v b/examples/glfw_vulkan/main.c.v index 8b08f0c..c626da9 100644 --- a/examples/glfw_vulkan/main.c.v +++ b/examples/glfw_vulkan/main.c.v @@ -14,6 +14,12 @@ fn C.v_imgui_example_draw_data_is_minimized(draw_data &imgui.ImDrawData) bool fn C.v_imgui_example_smoke_frame_limit() int +fn C.v_imgui_example_escape_reserved() bool + +fn C.v_imgui_example_escape_pressed() bool + +fn C.v_imgui_example_fullscreen_pressed() bool + // Hosts own Vulkan and GLFW; examples supply a frame callback and persistent state. pub fn main() { mut state := DemoState{} @@ -118,6 +124,8 @@ pub fn run(options Options, frame fn (mut App, voidptr), state voidptr) { // Let CI exercise rendering and orderly cleanup without closing the window manually. smoke_frame_limit := C.v_imgui_example_smoke_frame_limit() mut rendered_frames := 0 + mut window_mode := WindowMode{} + window_mode.remember_bounds(window) // Main loop for !glfw.window_should_close(window) { @@ -127,6 +135,8 @@ pub fn run(options Options, frame fn (mut App, voidptr), state voidptr) { // - When io.WantCaptureKeyboard is true, do not dispatch keyboard input data to your main application, or clear/overwrite your copy of the keyboard data. // Generally you may always pass all inputs to dear imgui, and hide them from your application based on those two flags. glfw.poll_events() + window_mode.remember_bounds(window) + escape_reserved := C.v_imgui_example_escape_reserved() // Resize swap chain? framebuffer := glfw.framebuffer_size(window) @@ -152,6 +162,7 @@ pub fn run(options Options, frame fn (mut App, voidptr), state voidptr) { if app.docking_available && app.dockspace_enabled { imgui.create_main_dockspace() } + app.escape_handled = false frame(mut app, state) // Rendering @@ -171,6 +182,13 @@ pub fn run(options Options, frame fn (mut App, voidptr), state voidptr) { if !is_minimized { app.frame_present(mut wd) } + if C.v_imgui_example_fullscreen_pressed() { + window_mode.toggle(window) + } + if C.v_imgui_example_escape_pressed() && !escape_reserved + && !C.v_imgui_example_escape_reserved() && !app.escape_handled { + glfw.set_window_should_close(window, 1) + } rendered_frames++ if smoke_frame_limit > 0 && rendered_frames >= smoke_frame_limit { break @@ -196,7 +214,9 @@ pub fn run(options Options, frame fn (mut App, voidptr), state voidptr) { pub struct App { pub mut: - native_window voidptr + native_window voidptr + // Set during the frame callback when an example consumes Escape itself. + escape_handled bool allocator &vk.AllocationCallbacks = unsafe { nil } instance vk.Instance physical_device vk.PhysicalDevice diff --git a/examples/glfw_vulkan/shortcuts.v b/examples/glfw_vulkan/shortcuts.v new file mode 100644 index 0000000..0663da1 --- /dev/null +++ b/examples/glfw_vulkan/shortcuts.v @@ -0,0 +1,78 @@ +module glfw_vulkan + +import antono2.glfw +import math + +// Keep a regular GLFW window, covering its monitor without changing video modes. +struct WindowMode { +mut: + fullscreen bool + x i32 + y i32 + width i32 + height i32 + decorated i32 + maximized bool +} + +fn (mut mode WindowMode) remember_bounds(window &glfw.Window) { + if mode.fullscreen || glfw.get_window_attrib(window, glfw.maximized) != 0 + || glfw.get_window_attrib(window, glfw.iconified) != 0 { + return + } + glfw.get_window_pos(window, &mode.x, &mode.y) + size := glfw.window_size(window) + mode.width = size.width + mode.height = size.height +} + +fn (mut mode WindowMode) toggle(window &glfw.Window) { + if mode.fullscreen { + glfw.set_window_attrib(window, glfw.decorated, mode.decorated) + glfw.set_window_monitor(window, unsafe { nil }, mode.x, mode.y, mode.width, mode.height, glfw.dont_care) + if mode.maximized { glfw.maximize_window(window) } + mode.fullscreen = false + return + } + mut x := i32(0) + mut y := i32(0) + glfw.get_window_pos(window, &x, &y) + size := glfw.window_size(window) + mut count := i32(0) + monitors := glfw.get_monitors(&count) + mut selected := glfw.get_primary_monitor() + mut largest := i64(-1) + for i in 0 .. count { + mut monitor := unsafe { monitors[i] } + video := glfw.get_video_mode(monitor) + if isnil(video) { + continue + } + mut mx := i32(0) + mut my := i32(0) + glfw.get_monitor_pos(monitor, &mx, &my) + overlap_width := i32(math.max(0, math.min(x + size.width, mx + video.width) - math.max(x, mx))) + overlap_height := i32(math.max(0, math.min(y + size.height, my + video.height) - math.max(y, my))) + overlap := i64(overlap_width) * overlap_height + if overlap > largest { + largest = overlap + selected = monitor + } + } + if isnil(selected) { + return + } + video := glfw.get_video_mode(selected) + if isnil(video) { + return + } + mut mx := i32(0) + mut my := i32(0) + glfw.get_monitor_pos(selected, &mx, &my) + mode.decorated = glfw.get_window_attrib(window, glfw.decorated) + mode.maximized = glfw.get_window_attrib(window, glfw.maximized) != 0 + if mode.maximized { glfw.restore_window(window) } + glfw.set_window_attrib(window, glfw.decorated, 0) + glfw.set_window_monitor(window, unsafe { nil }, mx, my, video.width, video.height, glfw.dont_care) + mode.fullscreen = true +} diff --git a/packaging/README-linux.txt b/packaging/README-linux.txt index f66c317..139e0db 100644 --- a/packaging/README-linux.txt +++ b/packaging/README-linux.txt @@ -9,6 +9,9 @@ To launch directly: ./run.sh implot_dashboard ./run.sh glfw_vulkan +In an example, Escape quits after active controls consume it to cancel an edit +or popup. F11 toggles borderless fullscreen and restores the previous window. + The examples, ImGui/ImPlot library, GLFW, Vulkan loader, C++ runtime, and non-system runtime dependencies are included. No V compiler, Vulkan SDK, or additional application libraries need to be installed. diff --git a/packaging/README-windows.txt b/packaging/README-windows.txt index f460942..eba0f50 100644 --- a/packaging/README-windows.txt +++ b/packaging/README-windows.txt @@ -9,6 +9,9 @@ You can also open an example directly: implot_dashboard.exe glfw_vulkan.exe +In an example, Escape quits after active controls consume it to cancel an edit +or popup. F11 toggles borderless fullscreen and restores the previous window. + ImGui/ImPlot, GLFW, the Vulkan loader, and the Microsoft Visual C++ runtime DLLs are included beside the applications. No compiler, SDK, or separate Visual C++ runtime installation is needed. Keep the whole folder together. diff --git a/tests/desktop/shortcuts.sh b/tests/desktop/shortcuts.sh new file mode 100755 index 0000000..60747e8 --- /dev/null +++ b/tests/desktop/shortcuts.sh @@ -0,0 +1,118 @@ +#!/usr/bin/env bash +set -euo pipefail +binary_dir=$(cd -- "${1:?Usage: shortcuts.sh binary-dir [--raw-gallery]}" && pwd) +# Always use a private display; never send keys to the user's desktop. +if [[ ${VIMGUI_SHORTCUT_TEST_SESSION:-0} != 1 ]]; then + exec env VIMGUI_SHORTCUT_TEST_SESSION=1 xvfb-run -a -s '-screen 0 1600x1000x24' bash "$0" "$binary_dir" "${2:-}" +fi +log_dir="$binary_dir/shortcut-logs" +mkdir -p "$log_dir" +app_pid='' +wm_pid='' +cleanup() { + if [[ -n "$app_pid" ]]; then kill "$app_pid" 2>/dev/null || true; fi + if [[ -n "$wm_pid" ]]; then kill "$wm_pid" 2>/dev/null || true; fi +} +trap cleanup EXIT +openbox --sm-disable > "$log_dir/window-manager.log" 2>&1 & +wm_pid=$! +for i in {1..100}; do + if xprop -root _NET_SUPPORTING_WM_CHECK 2>/dev/null | grep -q 'window id'; then break; fi + sleep 0.1 +done +window='' +start_app() { + "$binary_dir/$1" > "$log_dir/$1.log" 2>&1 & + app_pid=$! + window='' + for i in {1..100}; do + window=$(xdotool search --onlyvisible --name '^V ImGui:' 2>/dev/null | head -1 || true) + if [[ -n "$window" ]]; then break; fi + if ! kill -0 "$app_pid" 2>/dev/null; then cat "$log_dir/$1.log"; echo "Failed to open $1" >&2; exit 1; fi + sleep 0.1 + done + test -n "$window" + xdotool windowactivate --sync "$window" + sleep 0.3 +} +geometry() { + xdotool getwindowgeometry --shell "$window" | sed -n '/^X=/p; /^Y=/p; /^WIDTH=/p; /^HEIGHT=/p' +} +wait_geometry() { + for i in {1..100}; do + if [[ $(geometry) == "$1" ]]; then return; fi + sleep 0.05 + done + echo 'Window geometry did not reach expected state' >&2 + geometry >&2 + exit 1 +} +quit_app() { + xdotool key Escape + for i in {1..100}; do + if ! kill -0 "$app_pid" 2>/dev/null; then wait "$app_pid"; app_pid=''; return; fi + sleep 0.05 + done + echo 'Escape did not quit the example' >&2 + exit 1 +} +fullscreen=$'X=0\nY=0\nWIDTH=1600\nHEIGHT=1000' +for example in glfw_vulkan widget_gallery implot_dashboard; do + start_app "$example" + normal=$(geometry) + normal_frame=$(xprop -id "$window" _NET_FRAME_EXTENTS) + xdotool keydown F11 + wait_geometry "$fullscreen" + # Holding F11 must not toggle repeatedly. + sleep 0.6 + test "$(geometry)" == "$fullscreen" + xdotool keyup F11 + xprop -id "$window" _NET_FRAME_EXTENTS | grep -q '= 0, 0, 0, 0' + if xprop -id "$window" _NET_WM_STATE | grep -q '_NET_WM_STATE_FULLSCREEN'; then + echo 'Expected a regular borderless window, not exclusive fullscreen' >&2 + exit 1 + fi + sleep 0.1 + xdotool key F11 + wait_geometry "$normal" + test "$(xprop -id "$window" _NET_FRAME_EXTENTS)" == "$normal_frame" + quit_app + echo "PASS: $example — F11, held key, restore geometry and Escape" +done +start_app widget_gallery +normal=$(geometry) +wmctrl -i -r "$window" -b add,maximized_vert,maximized_horz +for i in {1..100}; do + if xprop -id "$window" _NET_WM_STATE | grep -q '_NET_WM_STATE_MAXIMIZED_VERT'; then break; fi + sleep 0.05 +done +sleep 0.2 +maximized=$(geometry) +test "$normal" != "$maximized" +xdotool key F11 +wait_geometry "$fullscreen" +xdotool key F11 +wait_geometry "$maximized" +wmctrl -i -r "$window" -b remove,maximized_vert,maximized_horz +wait_geometry "$normal" +quit_app +echo 'PASS: Fullscreen preserves maximized state and normal restore bounds' +if [[ ${2:-} == --raw-gallery ]]; then + start_app widget_gallery + # Raw gallery's fixed initial layout: edit the Name field. + xdotool mousemove --window "$window" 200 113 click 1 + xdotool type --clearmodifiers 'Escape cancels this edit' + xdotool key Escape + sleep 0.2 + kill -0 "$app_pid" + quit_app + echo 'PASS: Escape cancels editing before quitting' + start_app widget_gallery + xdotool mousemove --window "$window" 100 252 click 1 + sleep 0.2 + xdotool key Escape + sleep 0.2 + kill -0 "$app_pid" + quit_app + echo 'PASS: Escape closes a popup before quitting' +fi From 1be7f726bb34e97b6c82a52bb95440e3eb9736dd Mon Sep 17 00:00:00 2001 From: antono2 Date: Sat, 3 Oct 2026 08:17:42 +0200 Subject: [PATCH 12/29] Use the current monitor mode for windowed fullscreen and validate toggles --- .github/workflows/compile.yml | 2 +- .github/workflows/release-packages.yml | 2 +- docs/testing.md | 2 ++ examples/glfw_vulkan/main.c.v | 2 ++ examples/glfw_vulkan/shortcuts.v | 7 ++----- tests/desktop/shortcuts.sh | 22 +++++++++++++++++----- 6 files changed, 25 insertions(+), 12 deletions(-) diff --git a/.github/workflows/compile.yml b/.github/workflows/compile.yml index 8f156ab..fc4bb46 100644 --- a/.github/workflows/compile.yml +++ b/.github/workflows/compile.yml @@ -60,7 +60,7 @@ jobs: make local=1 echo "$GITHUB_WORKSPACE/toolchain/v" >> "$GITHUB_PATH" - name: Install native dependencies - run: sudo apt-get update && sudo apt-get install -y build-essential cmake libglfw3-dev libvulkan-dev libvulkan-volk-dev pkg-config mesa-vulkan-drivers vulkan-validationlayers xvfb openbox xdotool x11-utils wmctrl + run: sudo apt-get update && sudo apt-get install -y build-essential cmake libglfw3-dev libvulkan-dev libvulkan-volk-dev pkg-config mesa-vulkan-drivers vulkan-validationlayers xvfb openbox xdotool x11-utils x11-xserver-utils wmctrl - name: Build native ImGui library working-directory: source/modules/antono2/imgui run: v run build_vimgui.vsh --linkage shared --glfw system diff --git a/.github/workflows/release-packages.yml b/.github/workflows/release-packages.yml index 5aa3b84..b4a5f19 100644 --- a/.github/workflows/release-packages.yml +++ b/.github/workflows/release-packages.yml @@ -42,7 +42,7 @@ jobs: path: build/modules/antono2/glfw - name: Install Linux build and rendering tools if: runner.os == 'Linux' - run: sudo apt-get update && sudo apt-get install -y build-essential cmake libglfw3-dev libvulkan-dev libvulkan-volk-dev libdbus-1-dev libatk1.0-dev libatk-bridge2.0-dev libatspi2.0-dev libglib2.0-dev at-spi2-core dbus-x11 pkg-config mesa-vulkan-drivers vulkan-validationlayers xvfb patchelf zip xdotool openbox x11-utils wmctrl + run: sudo apt-get update && sudo apt-get install -y build-essential cmake libglfw3-dev libvulkan-dev libvulkan-volk-dev libdbus-1-dev libatk1.0-dev libatk-bridge2.0-dev libatspi2.0-dev libglib2.0-dev at-spi2-core dbus-x11 pkg-config mesa-vulkan-drivers vulkan-validationlayers xvfb patchelf zip xdotool openbox x11-utils x11-xserver-utils wmctrl - name: Install Windows Vulkan build tools if: runner.os == 'Windows' shell: bash diff --git a/docs/testing.md b/docs/testing.md index c3d2e17..d8d9137 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -36,6 +36,8 @@ scripts/smoke_desktop.sh "$PWD/build" Run `./tests/desktop/shortcuts.sh binary-dir --raw-gallery` for keyboard checks of source builds, or omit `--raw-gallery` for accessible release builds. The check uses its own Xvfb display with Openbox, xdotool, xprop and wmctrl. +It also checks that the display mode is unchanged and enables Vulkan validation +while toggling fullscreen. These keyboard checks were verified locally on Linux. ## Android interaction diff --git a/examples/glfw_vulkan/main.c.v b/examples/glfw_vulkan/main.c.v index c626da9..ba3f734 100644 --- a/examples/glfw_vulkan/main.c.v +++ b/examples/glfw_vulkan/main.c.v @@ -47,6 +47,8 @@ pub fn run(options Options, frame fn (mut App, voidptr), state voidptr) { // Create window with Vulkan context glfw.window_hint(glfw.client_api, glfw.no_api) + // Borderless fullscreen should remain available when switching applications. + glfw.window_hint(glfw.auto_iconify, 0) main_scale := f32(1.0) window := glfw.create_windowed(i32(1200 * main_scale), i32(800 * main_scale), options.title) or { diff --git a/examples/glfw_vulkan/shortcuts.v b/examples/glfw_vulkan/shortcuts.v index 0663da1..a0c6aef 100644 --- a/examples/glfw_vulkan/shortcuts.v +++ b/examples/glfw_vulkan/shortcuts.v @@ -3,7 +3,7 @@ module glfw_vulkan import antono2.glfw import math -// Keep a regular GLFW window, covering its monitor without changing video modes. +// Use the monitor's current video mode for GLFW's windowed fullscreen mode. struct WindowMode { mut: fullscreen bool @@ -66,13 +66,10 @@ fn (mut mode WindowMode) toggle(window &glfw.Window) { if isnil(video) { return } - mut mx := i32(0) - mut my := i32(0) - glfw.get_monitor_pos(selected, &mx, &my) mode.decorated = glfw.get_window_attrib(window, glfw.decorated) mode.maximized = glfw.get_window_attrib(window, glfw.maximized) != 0 if mode.maximized { glfw.restore_window(window) } glfw.set_window_attrib(window, glfw.decorated, 0) - glfw.set_window_monitor(window, unsafe { nil }, mx, my, video.width, video.height, glfw.dont_care) + glfw.set_window_monitor(window, selected, 0, 0, video.width, video.height, video.refreshRate) mode.fullscreen = true } diff --git a/tests/desktop/shortcuts.sh b/tests/desktop/shortcuts.sh index 60747e8..836d530 100755 --- a/tests/desktop/shortcuts.sh +++ b/tests/desktop/shortcuts.sh @@ -5,6 +5,11 @@ binary_dir=$(cd -- "${1:?Usage: shortcuts.sh binary-dir [--raw-gallery]}" && pwd if [[ ${VIMGUI_SHORTCUT_TEST_SESSION:-0} != 1 ]]; then exec env VIMGUI_SHORTCUT_TEST_SESSION=1 xvfb-run -a -s '-screen 0 1600x1000x24' bash "$0" "$binary_dir" "${2:-}" fi +lavapipe=(/usr/share/vulkan/icd.d/lvp_icd*.json) +test -f "${lavapipe[0]}" +export VK_DRIVER_FILES="${lavapipe[0]}" +export VK_ICD_FILENAMES="$VK_DRIVER_FILES" +export VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation log_dir="$binary_dir/shortcut-logs" mkdir -p "$log_dir" app_pid='' @@ -22,6 +27,7 @@ for i in {1..100}; do done window='' start_app() { + current_example=$1 "$binary_dir/$1" > "$log_dir/$1.log" 2>&1 & app_pid=$! window='' @@ -50,13 +56,22 @@ wait_geometry() { quit_app() { xdotool key Escape for i in {1..100}; do - if ! kill -0 "$app_pid" 2>/dev/null; then wait "$app_pid"; app_pid=''; return; fi + if ! kill -0 "$app_pid" 2>/dev/null; then + wait "$app_pid" + app_pid='' + if grep -Eiq 'VUID-|validation error|segmentation|assert|fatal' "$log_dir/$current_example.log"; then + cat "$log_dir/$current_example.log" >&2 + return 1 + fi + return + fi sleep 0.05 done echo 'Escape did not quit the example' >&2 exit 1 } fullscreen=$'X=0\nY=0\nWIDTH=1600\nHEIGHT=1000' +desktop_mode=$(xrandr --current) for example in glfw_vulkan widget_gallery implot_dashboard; do start_app "$example" normal=$(geometry) @@ -68,10 +83,7 @@ for example in glfw_vulkan widget_gallery implot_dashboard; do test "$(geometry)" == "$fullscreen" xdotool keyup F11 xprop -id "$window" _NET_FRAME_EXTENTS | grep -q '= 0, 0, 0, 0' - if xprop -id "$window" _NET_WM_STATE | grep -q '_NET_WM_STATE_FULLSCREEN'; then - echo 'Expected a regular borderless window, not exclusive fullscreen' >&2 - exit 1 - fi + test "$(xrandr --current)" == "$desktop_mode" sleep 0.1 xdotool key F11 wait_geometry "$normal" From 95d04a0d1e4395c488e3c53c888e0ee8a6a1c8be Mon Sep 17 00:00:00 2001 From: antono2 Date: Sat, 3 Oct 2026 08:33:47 +0200 Subject: [PATCH 13/29] Use native checkbox and radio actions in Windows accessibility probe --- tests/desktop/accessibility.ps1 | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/tests/desktop/accessibility.ps1 b/tests/desktop/accessibility.ps1 index 5e885cb..0fc7a77 100644 --- a/tests/desktop/accessibility.ps1 +++ b/tests/desktop/accessibility.ps1 @@ -20,27 +20,38 @@ function Find-Control([string] $Name) { } function Require-Control([string] $Name) { Wait-For { Find-Control $Name } "Missing UIA control: $Name" } function Invoke-Control([string] $Name) { + Write-Output "Invoke: $Name" $Node = Require-Control $Name $Pattern = $Node.GetCurrentPattern([System.Windows.Automation.InvokePattern]::Pattern) $Pattern.Invoke() } +function Toggle-Control([string] $Name) { + Write-Output "Toggle: $Name" + $Pattern = (Require-Control $Name).GetCurrentPattern([System.Windows.Automation.TogglePattern]::Pattern) + $Pattern.Toggle() + return $Pattern +} +function Select-Control([string] $Name) { + Write-Output "Select: $Name" + $Pattern = (Require-Control $Name).GetCurrentPattern([System.Windows.Automation.SelectionItemPattern]::Pattern) + $Pattern.Select() +} try { $Handle = Wait-For { $Process.Refresh(); if ($Process.MainWindowHandle -ne 0) { $Process.MainWindowHandle } } 'No gallery window' $Root = [System.Windows.Automation.AutomationElement]::FromHandle($Handle) foreach ($Name in @('Count', 'Name', 'High contrast', 'Files')) { $Node = Require-Control $Name; Write-Output "$Name : $($Node.Current.ControlType.ProgrammaticName)" } Invoke-Control 'Count'; $null = Require-Control 'Count: 1' - $Toggle = (Require-Control 'High contrast').GetCurrentPattern([System.Windows.Automation.TogglePattern]::Pattern) - $Toggle.Toggle() + $Toggle = Toggle-Control 'High contrast' $null = Wait-For { $Toggle.Current.ToggleState -eq [System.Windows.Automation.ToggleState]::On } 'Checkbox state did not update' - Invoke-Control '200% text'; Invoke-Control 'Focus last file' + $null = Toggle-Control '200% text'; Invoke-Control 'Focus last file' $Last = Require-Control 'Photo 0999.jpg' $Last.GetCurrentPattern([System.Windows.Automation.SelectionItemPattern]::Pattern).Select() $null = Require-Control 'Selected Photo 0999.jpg' $List = (Require-Control 'Files').GetCurrentPattern([System.Windows.Automation.ScrollPattern]::Pattern) $List.SetScrollPercent(-1, 50) $null = Wait-For { [Math]::Abs($List.Current.VerticalScrollPercent - 50) -lt 1 } 'Exact scroll percentage did not apply' - Invoke-Control 'Raw ImGui widgets'; $null = Require-Control 'Accessible controls' - Invoke-Control 'Accessible controls'; $null = Require-Control 'Name' + Select-Control 'Raw ImGui widgets'; $null = Require-Control 'Accessible controls' + Select-Control 'Accessible controls'; $null = Require-Control 'Name' $Unicode = 'A' + [char]0xd83d + [char]0xdcf7 + 'e' + [char]0x0301 + 'Z' $Field = Require-Control 'Name' $Value = $Field.GetCurrentPattern([System.Windows.Automation.ValuePattern]::Pattern) From 82c3dd5fa1428f0b7477abdc3ead26334f4b3423 Mon Sep 17 00:00:00 2001 From: antono2 Date: Sat, 3 Oct 2026 08:34:07 +0200 Subject: [PATCH 14/29] Wait for the gallery view selection before switching back --- tests/desktop/accessibility.ps1 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/desktop/accessibility.ps1 b/tests/desktop/accessibility.ps1 index 0fc7a77..67a4b49 100644 --- a/tests/desktop/accessibility.ps1 +++ b/tests/desktop/accessibility.ps1 @@ -50,7 +50,8 @@ try { $List = (Require-Control 'Files').GetCurrentPattern([System.Windows.Automation.ScrollPattern]::Pattern) $List.SetScrollPercent(-1, 50) $null = Wait-For { [Math]::Abs($List.Current.VerticalScrollPercent - 50) -lt 1 } 'Exact scroll percentage did not apply' - Select-Control 'Raw ImGui widgets'; $null = Require-Control 'Accessible controls' + Select-Control 'Raw ImGui widgets' + $null = Wait-For { (Require-Control 'Raw ImGui widgets').GetCurrentPattern([System.Windows.Automation.SelectionItemPattern]::Pattern).Current.IsSelected } 'Raw widget view did not open' Select-Control 'Accessible controls'; $null = Require-Control 'Name' $Unicode = 'A' + [char]0xd83d + [char]0xdcf7 + 'e' + [char]0x0301 + 'Z' $Field = Require-Control 'Name' From b8c9ed7e5a928e918f4e46e081dc84354bddfa37 Mon Sep 17 00:00:00 2001 From: antono2 Date: Sat, 3 Oct 2026 08:34:39 +0200 Subject: [PATCH 15/29] Bound the shortcut probe window activation wait --- tests/desktop/shortcuts.sh | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/desktop/shortcuts.sh b/tests/desktop/shortcuts.sh index 836d530..6ec3978 100755 --- a/tests/desktop/shortcuts.sh +++ b/tests/desktop/shortcuts.sh @@ -38,7 +38,12 @@ start_app() { sleep 0.1 done test -n "$window" - xdotool windowactivate --sync "$window" + if ! timeout 10s xdotool windowactivate --sync "$window"; then + echo "Window manager did not activate $current_example" >&2 + xprop -id "$window" _NET_WM_STATE >&2 || true + cat "$log_dir/window-manager.log" >&2 + exit 1 + fi sleep 0.3 } geometry() { From 2f26ccafbc86f388102a9d5be987eacf88e2c282 Mon Sep 17 00:00:00 2001 From: antono2 Date: Sat, 3 Oct 2026 08:35:10 +0200 Subject: [PATCH 16/29] Allow slower CI window startup and retain shortcut diagnostics --- .github/workflows/release-packages.yml | 4 +++- tests/desktop/shortcuts.sh | 9 +++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release-packages.yml b/.github/workflows/release-packages.yml index b4a5f19..825a15d 100644 --- a/.github/workflows/release-packages.yml +++ b/.github/workflows/release-packages.yml @@ -97,7 +97,9 @@ jobs: uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 with: name: render-${{ matrix.variant }} - path: build/v-imgui-examples-linux-*/smoke-logs + path: | + build/v-imgui-examples-linux-*/smoke-logs + build/v-imgui-examples-linux-*/shortcut-logs android: strategy: diff --git a/tests/desktop/shortcuts.sh b/tests/desktop/shortcuts.sh index 6ec3978..d569c0d 100755 --- a/tests/desktop/shortcuts.sh +++ b/tests/desktop/shortcuts.sh @@ -31,13 +31,18 @@ start_app() { "$binary_dir/$1" > "$log_dir/$1.log" 2>&1 & app_pid=$! window='' - for i in {1..100}; do + for i in {1..300}; do window=$(xdotool search --onlyvisible --name '^V ImGui:' 2>/dev/null | head -1 || true) if [[ -n "$window" ]]; then break; fi if ! kill -0 "$app_pid" 2>/dev/null; then cat "$log_dir/$1.log"; echo "Failed to open $1" >&2; exit 1; fi sleep 0.1 done - test -n "$window" + if [[ -z "$window" ]]; then + echo "No visible window appeared for $current_example within 30 seconds" >&2 + cat "$log_dir/$current_example.log" >&2 + cat "$log_dir/window-manager.log" >&2 + exit 1 + fi if ! timeout 10s xdotool windowactivate --sync "$window"; then echo "Window manager did not activate $current_example" >&2 xprop -id "$window" _NET_WM_STATE >&2 || true From 661193196e1b15f6bdd8390210db589ccf43cdb9 Mon Sep 17 00:00:00 2001 From: antono2 Date: Sat, 3 Oct 2026 08:42:37 +0200 Subject: [PATCH 17/29] Report native scroll percentages in the Windows probe --- tests/desktop/accessibility.ps1 | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/desktop/accessibility.ps1 b/tests/desktop/accessibility.ps1 index 67a4b49..67083ca 100644 --- a/tests/desktop/accessibility.ps1 +++ b/tests/desktop/accessibility.ps1 @@ -26,7 +26,7 @@ function Invoke-Control([string] $Name) { $Pattern.Invoke() } function Toggle-Control([string] $Name) { - Write-Output "Toggle: $Name" + Write-Host "Toggle: $Name" $Pattern = (Require-Control $Name).GetCurrentPattern([System.Windows.Automation.TogglePattern]::Pattern) $Pattern.Toggle() return $Pattern @@ -48,8 +48,11 @@ try { $Last.GetCurrentPattern([System.Windows.Automation.SelectionItemPattern]::Pattern).Select() $null = Require-Control 'Selected Photo 0999.jpg' $List = (Require-Control 'Files').GetCurrentPattern([System.Windows.Automation.ScrollPattern]::Pattern) + Write-Output "Scroll before request: $($List.Current.VerticalScrollPercent)% (view $($List.Current.VerticalViewSize)%)" $List.SetScrollPercent(-1, 50) - $null = Wait-For { [Math]::Abs($List.Current.VerticalScrollPercent - 50) -lt 1 } 'Exact scroll percentage did not apply' + try { + $null = Wait-For { [Math]::Abs($List.Current.VerticalScrollPercent - 50) -lt 1 } 'Exact scroll percentage did not apply' + } finally { Write-Output "Scroll after request: $($List.Current.VerticalScrollPercent)% (view $($List.Current.VerticalViewSize)%)" } Select-Control 'Raw ImGui widgets' $null = Wait-For { (Require-Control 'Raw ImGui widgets').GetCurrentPattern([System.Windows.Automation.SelectionItemPattern]::Pattern).Current.IsSelected } 'Raw widget view did not open' Select-Control 'Accessible controls'; $null = Require-Control 'Name' From c8c48a9e76953dad2366929a032cf0506daf2031 Mon Sep 17 00:00:00 2001 From: antono2 Date: Sat, 3 Oct 2026 08:44:41 +0200 Subject: [PATCH 18/29] Pin the Windows scroll property update for release verification --- .github/workflows/release-packages.yml | 4 ++-- IMGUI_REVISION | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release-packages.yml b/.github/workflows/release-packages.yml index 825a15d..9edbcd2 100644 --- a/.github/workflows/release-packages.yml +++ b/.github/workflows/release-packages.yml @@ -14,9 +14,9 @@ jobs: variant: [docking, standard] include: - variant: docking - imgui-ref: f13c1c8e6a79e81df9867155c589e5eeafa17b37 + imgui-ref: 29212746b9c6a5e0d52c4105d0f354ce0082c338 - variant: standard - imgui-ref: d66ee51490378ab651f1f184cff9eff0949dc20a + imgui-ref: 104cefe3956baac738a9150379d9adfe7ff49c14 runs-on: ${{ matrix.os }} timeout-minutes: 45 env: diff --git a/IMGUI_REVISION b/IMGUI_REVISION index 0246766..277be04 100644 --- a/IMGUI_REVISION +++ b/IMGUI_REVISION @@ -1 +1 @@ -f13c1c8e6a79e81df9867155c589e5eeafa17b37 +29212746b9c6a5e0d52c4105d0f354ce0082c338 From 9364e86a3753fc1551a1614899a431b6fc225437 Mon Sep 17 00:00:00 2001 From: antono2 Date: Sat, 3 Oct 2026 08:50:39 +0200 Subject: [PATCH 19/29] Keep the UIA wait helper from shadowing the text pattern --- tests/desktop/accessibility.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/desktop/accessibility.ps1 b/tests/desktop/accessibility.ps1 index 67083ca..6df60e1 100644 --- a/tests/desktop/accessibility.ps1 +++ b/tests/desktop/accessibility.ps1 @@ -8,8 +8,8 @@ function Wait-For([scriptblock] $Probe, [string] $Message) { $Deadline = [DateTime]::UtcNow.AddSeconds(30) do { if ($Process.HasExited) { throw "Gallery exited with $($Process.ExitCode): $Message" } - $Value = & $Probe - if ($Value) { return $Value } + $ProbeResult = & $Probe + if ($ProbeResult) { return $ProbeResult } Start-Sleep -Milliseconds 100 } while ([DateTime]::UtcNow -lt $Deadline) throw $Message From ee3c8f9970f090675e71631766274acbf19931b1 Mon Sep 17 00:00:00 2001 From: antono2 Date: Sat, 3 Oct 2026 08:56:54 +0200 Subject: [PATCH 20/29] Pin the virtual list text-scaling and scroll priority fix --- .github/workflows/release-packages.yml | 4 ++-- IMGUI_REVISION | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release-packages.yml b/.github/workflows/release-packages.yml index 9edbcd2..4b00dc5 100644 --- a/.github/workflows/release-packages.yml +++ b/.github/workflows/release-packages.yml @@ -14,9 +14,9 @@ jobs: variant: [docking, standard] include: - variant: docking - imgui-ref: 29212746b9c6a5e0d52c4105d0f354ce0082c338 + imgui-ref: fb026c199b0e75041b5dca4105e8084b1d4163fb - variant: standard - imgui-ref: 104cefe3956baac738a9150379d9adfe7ff49c14 + imgui-ref: a2e819c1d868c2ee9ddd74c4dccfce54532a8f35 runs-on: ${{ matrix.os }} timeout-minutes: 45 env: diff --git a/IMGUI_REVISION b/IMGUI_REVISION index 277be04..ac651c6 100644 --- a/IMGUI_REVISION +++ b/IMGUI_REVISION @@ -1 +1 @@ -29212746b9c6a5e0d52c4105d0f354ce0082c338 +fb026c199b0e75041b5dca4105e8084b1d4163fb From 14910401e9dc37ba406982f35004b3da60579d46 Mon Sep 17 00:00:00 2001 From: antono2 Date: Sat, 3 Oct 2026 09:03:56 +0200 Subject: [PATCH 21/29] Use the correct UI Automation text enum namespaces --- tests/desktop/accessibility.ps1 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/desktop/accessibility.ps1 b/tests/desktop/accessibility.ps1 index 6df60e1..8299fbd 100644 --- a/tests/desktop/accessibility.ps1 +++ b/tests/desktop/accessibility.ps1 @@ -63,9 +63,9 @@ try { $null = Wait-For { $Value.Current.Value -eq $Unicode } 'Unicode native edit did not round-trip' $Text = $Field.GetCurrentPattern([System.Windows.Automation.TextPattern]::Pattern) $Range = $Text.DocumentRange.Clone() - $Range.MoveEndpointByRange([System.Windows.Automation.TextPatternRangeEndpoint]::End, $Range, [System.Windows.Automation.TextPatternRangeEndpoint]::Start) - $null = $Range.MoveEndpointByUnit([System.Windows.Automation.TextPatternRangeEndpoint]::Start, [System.Windows.Automation.TextUnit]::Character, 1) - $null = $Range.MoveEndpointByUnit([System.Windows.Automation.TextPatternRangeEndpoint]::End, [System.Windows.Automation.TextUnit]::Character, 1) + $Range.MoveEndpointByRange([System.Windows.Automation.Text.TextPatternRangeEndpoint]::End, $Range, [System.Windows.Automation.Text.TextPatternRangeEndpoint]::Start) + $null = $Range.MoveEndpointByUnit([System.Windows.Automation.Text.TextPatternRangeEndpoint]::Start, [System.Windows.Automation.Text.TextUnit]::Character, 1) + $null = $Range.MoveEndpointByUnit([System.Windows.Automation.Text.TextPatternRangeEndpoint]::End, [System.Windows.Automation.Text.TextUnit]::Character, 1) $Range.Select() $Camera = [string][char]0xd83d + [char]0xdcf7 $null = Wait-For { $Selection = $Text.GetSelection(); $Selection.Count -eq 1 -and $Selection[0].GetText(-1) -eq $Camera } 'Unicode selection did not round-trip' From 888566e5b04c2e37183cdda49be86216f9b841a3 Mon Sep 17 00:00:00 2001 From: antono2 Date: Sat, 3 Oct 2026 09:27:07 +0200 Subject: [PATCH 22/29] Run the Windows UIA client in a multithreaded COM apartment --- .github/workflows/release-packages.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-packages.yml b/.github/workflows/release-packages.yml index 4b00dc5..aa22dbb 100644 --- a/.github/workflows/release-packages.yml +++ b/.github/workflows/release-packages.yml @@ -84,7 +84,7 @@ jobs: if: runner.os == 'Windows' shell: pwsh run: | - powershell.exe -NoProfile -ExecutionPolicy Bypass -File tests/desktop/accessibility.ps1 -Executable "build/v-imgui-examples-windows-x64-${{ matrix.variant }}/widget_gallery.exe" + powershell.exe -Mta -NoProfile -ExecutionPolicy Bypass -File tests/desktop/accessibility.ps1 -Executable "build/v-imgui-examples-windows-x64-${{ matrix.variant }}/widget_gallery.exe" if ($LASTEXITCODE -ne 0) { throw 'Native UI Automation check failed' } - name: Upload portable desktop package uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 From aa29c783a19ea65fb2493a2f54109b0d8bb025d7 Mon Sep 17 00:00:00 2001 From: antono2 Date: Sat, 3 Oct 2026 09:37:19 +0200 Subject: [PATCH 23/29] Capture the window tree and stack when CI startup fails --- tests/desktop/shortcuts.sh | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/desktop/shortcuts.sh b/tests/desktop/shortcuts.sh index d569c0d..1fa26c6 100755 --- a/tests/desktop/shortcuts.sh +++ b/tests/desktop/shortcuts.sh @@ -41,6 +41,12 @@ start_app() { echo "No visible window appeared for $current_example within 30 seconds" >&2 cat "$log_dir/$current_example.log" >&2 cat "$log_dir/window-manager.log" >&2 + xwininfo -root -tree >&2 || true + ps -o pid,stat,wchan:24,comm -p "$app_pid" >&2 || true + if command -v gdb >/dev/null; then + timeout 10s gdb -batch -ex 'set pagination off' -ex 'thread apply all bt' -p "$app_pid" > "$log_dir/$current_example-startup-stack.log" 2>&1 || true + cat "$log_dir/$current_example-startup-stack.log" >&2 + fi exit 1 fi if ! timeout 10s xdotool windowactivate --sync "$window"; then From 85dfaf1ed6ef5dfeb1f06aeea19750fd3424e7ee Mon Sep 17 00:00:00 2001 From: antono2 Date: Sat, 3 Oct 2026 09:50:56 +0200 Subject: [PATCH 24/29] Require rendering log scanner and enable CI startup stacks --- .github/workflows/release-packages.yml | 6 ++++-- scripts/smoke_desktop.sh | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release-packages.yml b/.github/workflows/release-packages.yml index aa22dbb..24c0cf0 100644 --- a/.github/workflows/release-packages.yml +++ b/.github/workflows/release-packages.yml @@ -42,7 +42,7 @@ jobs: path: build/modules/antono2/glfw - name: Install Linux build and rendering tools if: runner.os == 'Linux' - run: sudo apt-get update && sudo apt-get install -y build-essential cmake libglfw3-dev libvulkan-dev libvulkan-volk-dev libdbus-1-dev libatk1.0-dev libatk-bridge2.0-dev libatspi2.0-dev libglib2.0-dev at-spi2-core dbus-x11 pkg-config mesa-vulkan-drivers vulkan-validationlayers xvfb patchelf zip xdotool openbox x11-utils x11-xserver-utils wmctrl + run: sudo apt-get update && sudo apt-get install -y build-essential cmake libglfw3-dev libvulkan-dev libvulkan-volk-dev libdbus-1-dev libatk1.0-dev libatk-bridge2.0-dev libatspi2.0-dev libglib2.0-dev at-spi2-core dbus-x11 pkg-config mesa-vulkan-drivers vulkan-validationlayers ripgrep gdb xvfb patchelf zip xdotool openbox x11-utils x11-xserver-utils wmctrl - name: Install Windows Vulkan build tools if: runner.os == 'Windows' shell: bash @@ -70,7 +70,9 @@ jobs: GSETTINGS_BACKEND=memory dbus-run-session -- xvfb-run -a build/accessibility-check "build/v-imgui-examples-linux-x64-${{ matrix.variant }}/widget_gallery" "build/release-atspi-${{ matrix.variant }}.log" - name: Check bundled desktop keyboard shortcuts if: runner.os == 'Linux' - run: ./tests/desktop/shortcuts.sh "build/v-imgui-examples-linux-x64-${{ matrix.variant }}" + run: | + sudo sysctl kernel.yama.ptrace_scope=0 + ./tests/desktop/shortcuts.sh "build/v-imgui-examples-linux-x64-${{ matrix.variant }}" - name: Build and audit Windows release DLLs if: runner.os == 'Windows' shell: pwsh diff --git a/scripts/smoke_desktop.sh b/scripts/smoke_desktop.sh index 9a0995e..d17054f 100755 --- a/scripts/smoke_desktop.sh +++ b/scripts/smoke_desktop.sh @@ -1,6 +1,7 @@ #!/usr/bin/env bash set -euo pipefail binary_dir="${1:?Usage: scripts/smoke_desktop.sh path/to/binaries}" +command -v rg >/dev/null log_dir="$binary_dir/smoke-logs" mkdir -p "$log_dir" lavapipe=(/usr/share/vulkan/icd.d/lvp_icd*.json) From 4293d5cae6ed037cff7885c6add3e3b2c1818037 Mon Sep 17 00:00:00 2001 From: antono2 Date: Sat, 3 Oct 2026 09:52:18 +0200 Subject: [PATCH 25/29] Use standard grep for rendering log checks --- scripts/smoke_desktop.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/smoke_desktop.sh b/scripts/smoke_desktop.sh index d17054f..2b1d5eb 100755 --- a/scripts/smoke_desktop.sh +++ b/scripts/smoke_desktop.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -euo pipefail binary_dir="${1:?Usage: scripts/smoke_desktop.sh path/to/binaries}" -command -v rg >/dev/null +command -v grep >/dev/null log_dir="$binary_dir/smoke-logs" mkdir -p "$log_dir" lavapipe=(/usr/share/vulkan/icd.d/lvp_icd*.json) @@ -17,7 +17,7 @@ for example in glfw_vulkan widget_gallery implot_dashboard; do if [[ -x "$binary_dir/examples" ]]; then command=("$binary_dir/examples" "$example"); fi VIMGUI_SMOKE_FRAMES="$frames" timeout 60s xvfb-run -a "${command[@]}" > "$log_dir/$example.log" 2>&1 cat "$log_dir/$example.log" - if rg -iq 'VUID-|validation error|segmentation|assert|fatal' "$log_dir/$example.log"; then + if grep -Eiq 'VUID-|validation error|segmentation|assert|fatal' "$log_dir/$example.log"; then echo "Rendering failed: $example" >&2 exit 1 fi From b87ada5c29b6af1fec9708cd288663bb33c2cce6 Mon Sep 17 00:00:00 2001 From: antono2 Date: Sat, 3 Oct 2026 09:59:50 +0200 Subject: [PATCH 26/29] Check Unicode selection with native UIA and adopt portable upstream tooling --- .github/workflows/compile.yml | 2 +- .github/workflows/release-packages.yml | 11 ++++-- IMGUI_REVISION | 2 +- tests/desktop/accessibility.ps1 | 14 +++---- tests/desktop/windows_text.cpp | 53 ++++++++++++++++++++++++++ 5 files changed, 67 insertions(+), 15 deletions(-) create mode 100644 tests/desktop/windows_text.cpp diff --git a/.github/workflows/compile.yml b/.github/workflows/compile.yml index fc4bb46..eab3c71 100644 --- a/.github/workflows/compile.yml +++ b/.github/workflows/compile.yml @@ -130,7 +130,7 @@ jobs: "$env:GITHUB_WORKSPACE\toolchain\v" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append - name: Install Vulkan SDK and SwiftShader test artifacts shell: bash - run: bash source/modules/antono2/imgui/.github/scripts/setup-vulkan-ci.sh 1.4.309.0 v1.2 + run: v run source/modules/antono2/imgui/scripts/setup_vulkan_ci.vsh 1.4.309.0 v1.2 - name: Install matching Volk header shell: pwsh run: | diff --git a/.github/workflows/release-packages.yml b/.github/workflows/release-packages.yml index 24c0cf0..ec7513d 100644 --- a/.github/workflows/release-packages.yml +++ b/.github/workflows/release-packages.yml @@ -14,9 +14,9 @@ jobs: variant: [docking, standard] include: - variant: docking - imgui-ref: fb026c199b0e75041b5dca4105e8084b1d4163fb + imgui-ref: 711f5ecc0ff640fd843b2beef7d58d5875ab6cf6 - variant: standard - imgui-ref: a2e819c1d868c2ee9ddd74c4dccfce54532a8f35 + imgui-ref: e2af1b90fd86cd014cad9a3c0623aa5895c31f77 runs-on: ${{ matrix.os }} timeout-minutes: 45 env: @@ -46,7 +46,7 @@ jobs: - name: Install Windows Vulkan build tools if: runner.os == 'Windows' shell: bash - run: bash build/modules/antono2/imgui/.github/scripts/setup-vulkan-ci.sh 1.4.309.0 v1.2 + run: v run build/modules/antono2/imgui/scripts/setup_vulkan_ci.vsh 1.4.309.0 v1.2 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 if: runner.os == 'Windows' with: @@ -86,7 +86,10 @@ jobs: if: runner.os == 'Windows' shell: pwsh run: | - powershell.exe -Mta -NoProfile -ExecutionPolicy Bypass -File tests/desktop/accessibility.ps1 -Executable "build/v-imgui-examples-windows-x64-${{ matrix.variant }}/widget_gallery.exe" + . scripts/setup_windows_build.ps1 + cl.exe /nologo /std:c++17 /EHsc tests/desktop/windows_text.cpp /Febuild/windows-text-check.exe /Fobuild/windows-text-check.obj /link uiautomationcore.lib ole32.lib oleaut32.lib + if ($LASTEXITCODE -ne 0) { throw 'Native text probe build failed' } + powershell.exe -Mta -NoProfile -ExecutionPolicy Bypass -File tests/desktop/accessibility.ps1 -Executable "build/v-imgui-examples-windows-x64-${{ matrix.variant }}/widget_gallery.exe" -SelectionProbe "$PWD/build/windows-text-check.exe" if ($LASTEXITCODE -ne 0) { throw 'Native UI Automation check failed' } - name: Upload portable desktop package uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 diff --git a/IMGUI_REVISION b/IMGUI_REVISION index ac651c6..f1ecb31 100644 --- a/IMGUI_REVISION +++ b/IMGUI_REVISION @@ -1 +1 @@ -fb026c199b0e75041b5dca4105e8084b1d4163fb +711f5ecc0ff640fd843b2beef7d58d5875ab6cf6 diff --git a/tests/desktop/accessibility.ps1 b/tests/desktop/accessibility.ps1 index 8299fbd..6504845 100644 --- a/tests/desktop/accessibility.ps1 +++ b/tests/desktop/accessibility.ps1 @@ -1,4 +1,4 @@ -param([Parameter(Mandatory)] [string] $Executable) +param([Parameter(Mandatory)] [string] $Executable, [Parameter(Mandatory)] [string] $SelectionProbe) $ErrorActionPreference = 'Stop' Add-Type -AssemblyName UIAutomationClient Add-Type -AssemblyName UIAutomationTypes @@ -61,14 +61,10 @@ try { $Value = $Field.GetCurrentPattern([System.Windows.Automation.ValuePattern]::Pattern) $Value.SetValue($Unicode) $null = Wait-For { $Value.Current.Value -eq $Unicode } 'Unicode native edit did not round-trip' - $Text = $Field.GetCurrentPattern([System.Windows.Automation.TextPattern]::Pattern) - $Range = $Text.DocumentRange.Clone() - $Range.MoveEndpointByRange([System.Windows.Automation.Text.TextPatternRangeEndpoint]::End, $Range, [System.Windows.Automation.Text.TextPatternRangeEndpoint]::Start) - $null = $Range.MoveEndpointByUnit([System.Windows.Automation.Text.TextPatternRangeEndpoint]::Start, [System.Windows.Automation.Text.TextUnit]::Character, 1) - $null = $Range.MoveEndpointByUnit([System.Windows.Automation.Text.TextPatternRangeEndpoint]::End, [System.Windows.Automation.Text.TextUnit]::Character, 1) - $Range.Select() - $Camera = [string][char]0xd83d + [char]0xdcf7 - $null = Wait-For { $Selection = $Text.GetSelection(); $Selection.Count -eq 1 -and $Selection[0].GetText(-1) -eq $Camera } 'Unicode selection did not round-trip' + # The legacy .NET selected-range wrapper crashes even against system RichEdit + # on this runner. Exercise the same selection round trip with native UIA. + & $SelectionProbe $Handle.ToInt64().ToString() + if ($LASTEXITCODE -ne 0) { throw 'Native Unicode selection check failed' } Write-Output 'PASS: native UIA roles, actions, checkbox state, text scaling, virtual selection, exact scrolling, views and Unicode text/selection' } finally { if (-not $Process.HasExited) { Stop-Process -Id $Process.Id -Force } diff --git a/tests/desktop/windows_text.cpp b/tests/desktop/windows_text.cpp new file mode 100644 index 0000000..fe962b1 --- /dev/null +++ b/tests/desktop/windows_text.cpp @@ -0,0 +1,53 @@ +// Use the native UIA client API for Unicode selection round trips. +#define NOMINMAX +#include +#include +#include +#include +#include +template struct Com { + T *value=nullptr; + ~Com(){if(value)value->Release();} + T **out(){return &value;} + T *operator->(){return value;} +}; +static void check(HRESULT result) { + if(FAILED(result))throw std::runtime_error("UIA HRESULT "+std::to_string(static_cast(result))); +} +static std::wstring content(IUIAutomationTextRange *range) { + BSTR value=nullptr;check(range->GetText(-1,&value)); + std::wstring result(value?value:L"",value?SysStringLen(value):0);SysFreeString(value);return result; +} +int main(int argc,char **argv) { + if(argc!=2)return 2; + check(CoInitializeEx(nullptr,COINIT_MULTITHREADED)); + int result=0; + try { + HWND window=reinterpret_cast(std::stoull(argv[1])); + Com automation;check(CoCreateInstance(__uuidof(CUIAutomation),nullptr,CLSCTX_INPROC_SERVER,IID_PPV_ARGS(automation.out()))); + Com root;check(automation->ElementFromHandle(window,root.out())); + VARIANT name;VariantInit(&name);name.vt=VT_BSTR;name.bstrVal=SysAllocString(L"Name"); + Com condition;check(automation->CreatePropertyCondition(UIA_NamePropertyId,name,condition.out()));VariantClear(&name); + Com field;check(root->FindFirst(TreeScope_Descendants,condition.value,field.out())); + if(!field.value)throw std::runtime_error("Missing Name text field"); + Com text;check(field->GetCurrentPatternAs(UIA_TextPatternId,IID_PPV_ARGS(text.out()))); + Com document;check(text->get_DocumentRange(document.out())); + if(content(document.value)!=L"A\xd83d\xdcf7" L"e\x0301Z")throw std::runtime_error("Unicode document text mismatch"); + Com range;check(document->Clone(range.out())); + check(range->MoveEndpointByRange(TextPatternRangeEndpoint_End,range.value,TextPatternRangeEndpoint_Start)); + int moved=0;check(range->MoveEndpointByUnit(TextPatternRangeEndpoint_Start,TextUnit_Character,1,&moved)); + check(range->MoveEndpointByUnit(TextPatternRangeEndpoint_End,TextUnit_Character,1,&moved)); + if(content(range.value)!=L"\xd83d\xdcf7")throw std::runtime_error("Unicode moved range mismatch"); + check(range->Select()); + bool matched=false; + for(int attempt=0;attempt<300&&!matched;++attempt) { + Com selection;check(text->GetSelection(selection.out())); + int count=0;check(selection->get_Length(&count)); + if(count==1){Com selected;check(selection->GetElement(0,selected.out()));matched=content(selected.value)==L"\xd83d\xdcf7";} + if(!matched)Sleep(100); + } + if(!matched)throw std::runtime_error("Unicode selection did not round-trip"); + std::puts("PASS: native UIA Unicode document, moved range and selected text"); + }catch(const std::exception &error){std::fprintf(stderr,"%s\n",error.what());result=1;} + CoUninitialize();return result; +} From 56395bfaa80d12dbc35b438f0f272a8cbd84ae67 Mon Sep 17 00:00:00 2001 From: antono2 Date: Sat, 3 Oct 2026 10:02:22 +0200 Subject: [PATCH 27/29] Include the ATK cleanup fix in release builds --- .github/workflows/release-packages.yml | 4 ++-- IMGUI_REVISION | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release-packages.yml b/.github/workflows/release-packages.yml index ec7513d..878eaad 100644 --- a/.github/workflows/release-packages.yml +++ b/.github/workflows/release-packages.yml @@ -14,9 +14,9 @@ jobs: variant: [docking, standard] include: - variant: docking - imgui-ref: 711f5ecc0ff640fd843b2beef7d58d5875ab6cf6 + imgui-ref: 655006c3d07d7ae9136dc170f1fdcbdc4a6feae1 - variant: standard - imgui-ref: e2af1b90fd86cd014cad9a3c0623aa5895c31f77 + imgui-ref: ee39af15bc8a9ac3e27102557f37f514b30cf9f1 runs-on: ${{ matrix.os }} timeout-minutes: 45 env: diff --git a/IMGUI_REVISION b/IMGUI_REVISION index f1ecb31..5960fd4 100644 --- a/IMGUI_REVISION +++ b/IMGUI_REVISION @@ -1 +1 @@ -711f5ecc0ff640fd843b2beef7d58d5875ab6cf6 +655006c3d07d7ae9136dc170f1fdcbdc4a6feae1 From 8f5c924166d014326f656a9bad020ea15f2f25d1 Mon Sep 17 00:00:00 2001 From: antono2 Date: Sat, 3 Oct 2026 10:13:54 +0200 Subject: [PATCH 28/29] Use current upstream revisions in all source checks --- .github/workflows/additional-examples.yml | 2 +- .github/workflows/compile.yml | 6 +++--- docs/testing.md | 4 ++++ tests/desktop/accessibility.ps1 | 2 +- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/.github/workflows/additional-examples.yml b/.github/workflows/additional-examples.yml index 9b50e7f..88b7c4b 100644 --- a/.github/workflows/additional-examples.yml +++ b/.github/workflows/additional-examples.yml @@ -26,7 +26,7 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: repository: antono2/imgui - ref: 253cb56d86ab6bb79283980d57ea42e64f091734 + ref: ee39af15bc8a9ac3e27102557f37f514b30cf9f1 path: build/modules/antono2/imgui submodules: recursive - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 diff --git a/.github/workflows/compile.yml b/.github/workflows/compile.yml index eab3c71..31ff0af 100644 --- a/.github/workflows/compile.yml +++ b/.github/workflows/compile.yml @@ -26,7 +26,7 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: repository: antono2/imgui - ref: c093593eeb5685ab4c40b308b9773b51fec37ffe + ref: 655006c3d07d7ae9136dc170f1fdcbdc4a6feae1 path: source/modules/antono2/imgui submodules: recursive - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 @@ -92,7 +92,7 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: repository: antono2/imgui - ref: c093593eeb5685ab4c40b308b9773b51fec37ffe + ref: 655006c3d07d7ae9136dc170f1fdcbdc4a6feae1 path: source/modules/antono2/imgui submodules: recursive - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 @@ -204,7 +204,7 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: repository: antono2/imgui - ref: c093593eeb5685ab4c40b308b9773b51fec37ffe + ref: 655006c3d07d7ae9136dc170f1fdcbdc4a6feae1 path: source/modules/antono2/imgui submodules: recursive - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 diff --git a/docs/testing.md b/docs/testing.md index d8d9137..b4abfb7 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -74,6 +74,10 @@ and view switching. Windows bundles are checked for missing imported runtime DLLs. The Windows workflow also opens the packaged gallery in a runner's user session and checks UI Automation actions, text/selection, list selection and exact scroll percentages. Narrator speech still needs manual validation. +Unicode selected-text reads use the native `IUIAutomation` client API. The +legacy .NET selected-range wrapper also crashes against Windows' built-in +RichEdit on this runner; the comparison and reproduction instructions are in +[upstream's text checks](https://github.com/antono2/imgui/blob/master/tests/accessibility/README.md). On the connected Android tablet, the integrated accessibility build passed native roles/actions, checkbox state, progress ranges, Unicode text and UTF-16 selection, diff --git a/tests/desktop/accessibility.ps1 b/tests/desktop/accessibility.ps1 index 6504845..ec061ca 100644 --- a/tests/desktop/accessibility.ps1 +++ b/tests/desktop/accessibility.ps1 @@ -63,7 +63,7 @@ try { $null = Wait-For { $Value.Current.Value -eq $Unicode } 'Unicode native edit did not round-trip' # The legacy .NET selected-range wrapper crashes even against system RichEdit # on this runner. Exercise the same selection round trip with native UIA. - & $SelectionProbe $Handle.ToInt64().ToString() + & $SelectionProbe ($Handle.ToInt64().ToString()) if ($LASTEXITCODE -ne 0) { throw 'Native Unicode selection check failed' } Write-Output 'PASS: native UIA roles, actions, checkbox state, text scaling, virtual selection, exact scrolling, views and Unicode text/selection' } finally { From 85cf5543ed16045014e4b713d1c68353bab6f409 Mon Sep 17 00:00:00 2001 From: antono2 Date: Sat, 3 Oct 2026 10:25:13 +0200 Subject: [PATCH 29/29] Show initialized windows after draining startup events --- .github/workflows/additional-examples.yml | 2 +- .github/workflows/compile.yml | 6 +++--- .github/workflows/release-packages.yml | 4 ++-- IMGUI_REVISION | 2 +- examples/glfw_vulkan/main.c.v | 6 ++++++ tests/desktop/shortcuts.sh | 2 ++ 6 files changed, 15 insertions(+), 7 deletions(-) diff --git a/.github/workflows/additional-examples.yml b/.github/workflows/additional-examples.yml index 88b7c4b..ba48b04 100644 --- a/.github/workflows/additional-examples.yml +++ b/.github/workflows/additional-examples.yml @@ -26,7 +26,7 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: repository: antono2/imgui - ref: ee39af15bc8a9ac3e27102557f37f514b30cf9f1 + ref: 48d6c6614ead130450791a72179f711f4090a7e8 path: build/modules/antono2/imgui submodules: recursive - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 diff --git a/.github/workflows/compile.yml b/.github/workflows/compile.yml index 31ff0af..f31064b 100644 --- a/.github/workflows/compile.yml +++ b/.github/workflows/compile.yml @@ -26,7 +26,7 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: repository: antono2/imgui - ref: 655006c3d07d7ae9136dc170f1fdcbdc4a6feae1 + ref: fa2e194d447a4cb0975126b1d83e45889e7cadd9 path: source/modules/antono2/imgui submodules: recursive - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 @@ -92,7 +92,7 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: repository: antono2/imgui - ref: 655006c3d07d7ae9136dc170f1fdcbdc4a6feae1 + ref: fa2e194d447a4cb0975126b1d83e45889e7cadd9 path: source/modules/antono2/imgui submodules: recursive - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 @@ -204,7 +204,7 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: repository: antono2/imgui - ref: 655006c3d07d7ae9136dc170f1fdcbdc4a6feae1 + ref: fa2e194d447a4cb0975126b1d83e45889e7cadd9 path: source/modules/antono2/imgui submodules: recursive - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 diff --git a/.github/workflows/release-packages.yml b/.github/workflows/release-packages.yml index 878eaad..e6b77ff 100644 --- a/.github/workflows/release-packages.yml +++ b/.github/workflows/release-packages.yml @@ -14,9 +14,9 @@ jobs: variant: [docking, standard] include: - variant: docking - imgui-ref: 655006c3d07d7ae9136dc170f1fdcbdc4a6feae1 + imgui-ref: fa2e194d447a4cb0975126b1d83e45889e7cadd9 - variant: standard - imgui-ref: ee39af15bc8a9ac3e27102557f37f514b30cf9f1 + imgui-ref: 48d6c6614ead130450791a72179f711f4090a7e8 runs-on: ${{ matrix.os }} timeout-minutes: 45 env: diff --git a/IMGUI_REVISION b/IMGUI_REVISION index 5960fd4..e2b9f21 100644 --- a/IMGUI_REVISION +++ b/IMGUI_REVISION @@ -1 +1 @@ -655006c3d07d7ae9136dc170f1fdcbdc4a6feae1 +fa2e194d447a4cb0975126b1d83e45889e7cadd9 diff --git a/examples/glfw_vulkan/main.c.v b/examples/glfw_vulkan/main.c.v index ba3f734..5a4ad89 100644 --- a/examples/glfw_vulkan/main.c.v +++ b/examples/glfw_vulkan/main.c.v @@ -47,6 +47,8 @@ pub fn run(options Options, frame fn (mut App, voidptr), state voidptr) { // Create window with Vulkan context glfw.window_hint(glfw.client_api, glfw.no_api) + // Map the window after its renderer and UI are ready. + glfw.window_hint(glfw.visible, 0) // Borderless fullscreen should remain available when switching applications. glfw.window_hint(glfw.auto_iconify, 0) @@ -120,6 +122,10 @@ pub fn run(options Options, frame fn (mut App, voidptr), state voidptr) { impl_vulkan.vkinit(&init_info) options.initialize(state) + // Drain startup events before GLFW's X11 visibility wait. Queued unrelated + // events can prevent that wait from reaching its timeout during creation. + glfw.poll_events() + glfw.show_window(window) // No custom fonts are loaded, so Dear ImGui uses its default font. diff --git a/tests/desktop/shortcuts.sh b/tests/desktop/shortcuts.sh index 1fa26c6..1a3f5b2 100755 --- a/tests/desktop/shortcuts.sh +++ b/tests/desktop/shortcuts.sh @@ -46,6 +46,8 @@ start_app() { if command -v gdb >/dev/null; then timeout 10s gdb -batch -ex 'set pagination off' -ex 'thread apply all bt' -p "$app_pid" > "$log_dir/$current_example-startup-stack.log" 2>&1 || true cat "$log_dir/$current_example-startup-stack.log" >&2 + timeout 10s gdb -batch -ex 'set pagination off' -ex 'thread apply all bt' -p "$wm_pid" > "$log_dir/window-manager-stack.log" 2>&1 || true + cat "$log_dir/window-manager-stack.log" >&2 fi exit 1 fi