Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
77257a3
Initial plan
Copilot Feb 28, 2026
4bec7a8
Fix element bounds validation and safe double-to-int conversion
Copilot Feb 28, 2026
9edf129
Extract safe_double_to_int to shared bounds_util.h and fix screenshot…
Copilot Feb 28, 2026
fb440f4
Extract safe_double_to_int to shared bounds_util.h and fix screenshot…
Copilot Feb 28, 2026
29f3095
Remove CodeQL artifact and add to gitignore
Copilot Feb 28, 2026
a6725e3
Return std::optional from safe_double_to_int, add annotations test ho…
Copilot Mar 1, 2026
aba9c37
Use named constants for bounds thresholds, match annotate_pixels clip…
Copilot Mar 1, 2026
69bb3a6
Guard --annotations-json flag with #ifndef NDEBUG for debug-only builds
Copilot Mar 1, 2026
ebbe001
Add controlled Win32 window integration tests for tree structure, bou…
Copilot Mar 1, 2026
5e8898d
Fix integration test file locking: close ifstream before fs::remove
asklar Mar 1, 2026
65265c5
Fix WinUI3 element bounds: layout offset computation and bridge matching
asklar Mar 1, 2026
c4df9cb
Remove unreliable layout offset heuristic, add DPI scaling
asklar Mar 2, 2026
9afb265
Fix annotation bounds, exclude own terminal from --title, add build s…
asklar Mar 2, 2026
3d39a10
Fix XAML element positions via TransformToVisual and DPI scaling
asklar Mar 2, 2026
802c42a
Extract important XAML properties into tree dump
asklar Mar 2, 2026
e0dd59c
Use C++/WinRT projected types for XAML element inspection
asklar Mar 2, 2026
4c845c9
Fix text property: don't use AutomationProperties.Name as text, remov…
asklar Mar 2, 2026
089c5d6
Reduce annotation clutter: skip layout containers, place labels insid…
asklar Mar 2, 2026
f92e151
Fix CI: generate WinUI3 C++/WinRT headers at configure time
asklar Mar 2, 2026
8766bdd
Support UWP/system XAML (Windows.UI.Xaml) for position and text reading
asklar Mar 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ jobs:
with:
vcpkgGitCommitId: b1b19307e2d2ec1eefbdb7ea069de7d4bcd31f01

- name: Install Windows App SDK (for WinUI3 C++/WinRT headers)
run: nuget install Microsoft.WindowsAppSDK -Version 1.5.240607001 -OutputDirectory packages

- name: Configure
run: cmake --preset default

Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,6 @@ Desktop.ini
.DS_Store
*.png
!docs/*.png
_codeql_detected_source_root

src/tap/winui3/
76 changes: 74 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,84 @@

# TAP DLL — injected into target process for XAML diagnostics
# Uses static CRT (/MT) to avoid CRT version conflicts in the target process

# Generate C++/WinRT projected headers from the Windows App SDK winmd.
# These are needed for TransformToVisual / TextBlock.Text in the TAP DLL.
set(WINUI3_CPPWINRT_DIR "${CMAKE_SOURCE_DIR}/src/tap/winui3")
set(WINUI3_CPPWINRT_STAMP "${WINUI3_CPPWINRT_DIR}/winrt/Microsoft.UI.Xaml.h")
if(NOT EXISTS "${WINUI3_CPPWINRT_STAMP}")
# Find cppwinrt.exe from Windows SDK
find_program(CPPWINRT_EXE cppwinrt
HINTS "$ENV{WindowsSdkDir}/bin/${CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION}/x64"
"$ENV{WindowsSdkDir}/bin/10.0.22621.0/x64"
"C:/Program Files (x86)/Windows Kits/10/bin/10.0.22621.0/x64"
)
if(NOT CPPWINRT_EXE)
# Fallback: search PATH
find_program(CPPWINRT_EXE cppwinrt)
endif()

# Find Windows App SDK NuGet package (winmd files).
# Search NuGet global cache and local packages/ dir (CI installs here).
set(WASDK_WINMD_DIR "")
file(GLOB _wasdk_candidates
"$ENV{USERPROFILE}/.nuget/packages/microsoft.windowsappsdk/*/lib/uap10.0"
"$ENV{NUGET_PACKAGES}/microsoft.windowsappsdk/*/lib/uap10.0"
"${CMAKE_SOURCE_DIR}/packages/Microsoft.WindowsAppSDK.*/lib/uap10.0"
)
foreach(_candidate ${_wasdk_candidates})
if(EXISTS "${_candidate}/Microsoft.UI.Xaml.winmd")
set(WASDK_WINMD_DIR "${_candidate}")
break()
endif()
endforeach()

# Find Windows.winmd for system type references
set(WIN_UNION_WINMD "")
if(DEFINED ENV{WindowsSdkDir})
file(GLOB _union "$ENV{WindowsSdkDir}/UnionMetadata/*/Windows.winmd")
if(_union)
list(SORT _union ORDER DESCENDING)
list(GET _union 0 WIN_UNION_WINMD)
endif()
endif()
if(NOT WIN_UNION_WINMD)
file(GLOB _union "C:/Program Files (x86)/Windows Kits/10/UnionMetadata/*/Windows.winmd")
if(_union)
list(SORT _union ORDER DESCENDING)
list(GET _union 0 WIN_UNION_WINMD)
endif()
endif()

if(CPPWINRT_EXE AND WASDK_WINMD_DIR AND WIN_UNION_WINMD)
message(STATUS "Generating WinUI3 C++/WinRT headers from ${WASDK_WINMD_DIR}")
file(GLOB WASDK_ALL_WINMDS "${WASDK_WINMD_DIR}/*.winmd")
# Also include other winmd directories in the package
get_filename_component(_wasdk_root "${WASDK_WINMD_DIR}/../.." ABSOLUTE)
file(GLOB_RECURSE _extra_winmds "${_wasdk_root}/*.winmd")
list(APPEND WASDK_ALL_WINMDS ${_extra_winmds})
list(REMOVE_DUPLICATES WASDK_ALL_WINMDS)
execute_process(
COMMAND "${CPPWINRT_EXE}" -in ${WASDK_ALL_WINMDS} -ref "${WIN_UNION_WINMD}" -out "${WINUI3_CPPWINRT_DIR}"
RESULT_VARIABLE _cppwinrt_result
)
if(_cppwinrt_result EQUAL 0)
message(STATUS "WinUI3 C++/WinRT headers generated in ${WINUI3_CPPWINRT_DIR}")
else()
message(WARNING "cppwinrt failed (${_cppwinrt_result}); TAP DLL WinUI3 features will be limited")

Check warning on line 120 in CMakeLists.txt

View workflow job for this annotation

GitHub Actions / build-and-test

cppwinrt failed (1); TAP DLL WinUI3 features will be limited
endif()
else()
message(WARNING "Cannot generate WinUI3 C++/WinRT headers:"
" cppwinrt=${CPPWINRT_EXE} winmd_dir=${WASDK_WINMD_DIR} union=${WIN_UNION_WINMD}")
endif()
endif()

add_library(lvt_tap SHARED
src/tap/lvt_tap.cpp
src/tap/lvt_tap.def
)
target_compile_definitions(lvt_tap PRIVATE WIN32_LEAN_AND_MEAN NOMINMAX)
target_include_directories(lvt_tap PRIVATE src)
target_compile_definitions(lvt_tap PRIVATE WIN32_LEAN_AND_MEAN NOMINMAX WINRT_LEAN_AND_MEAN)
target_include_directories(lvt_tap PRIVATE src src/tap/winui3)
target_link_libraries(lvt_tap PRIVATE ole32)
set_property(TARGET lvt_tap PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")

Expand Down
67 changes: 67 additions & 0 deletions build.cmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
@echo off
setlocal enabledelayedexpansion

REM lvt build script — run from a VS Developer Command Prompt (x64)
REM Requires: VCPKG_ROOT set, cl.exe and ninja.exe on PATH
REM Usage: build.cmd [clean]

if "%VCPKG_ROOT%"=="" (
echo ERROR: VCPKG_ROOT is not set.
exit /b 1
)

where cl.exe >nul 2>&1 || (echo ERROR: cl.exe not found. Run from a VS Developer Command Prompt. & exit /b 1)
where ninja.exe >nul 2>&1 || (echo ERROR: ninja.exe not found. & exit /b 1)

if /i "%1"=="clean" (
echo Cleaning build directory...
if exist build rmdir /s /q build
)

REM --- .NET projects (must build before CMake) ---

echo.
echo === Building .NET projects ===

echo [1/2] LvtWpfTap (net48)...
dotnet build src\tap_wpf\LvtWpfTap.csproj -c Release -v:q --nologo
if errorlevel 1 (
echo WARNING: LvtWpfTap build failed (WPF TAP will be unavailable)
)

echo [2/2] LvtAvaloniaTreeWalker (net8.0)...
dotnet restore src\plugin_avalonia\LvtAvaloniaTreeWalker\LvtAvaloniaTreeWalker.csproj -v:q --nologo
dotnet publish src\plugin_avalonia\LvtAvaloniaTreeWalker\LvtAvaloniaTreeWalker.csproj -c Release -v:q --nologo
if errorlevel 1 (
echo WARNING: LvtAvaloniaTreeWalker build failed (Avalonia plugin will be unavailable)
)

REM --- CMake configure + build ---

echo.
echo === Configuring CMake (x64) ===

if not exist build (
cmake --preset default
if errorlevel 1 (
echo ERROR: CMake configure failed.
exit /b 1
)
)

echo.
echo === Building C++ targets ===

cmake --build build
if errorlevel 1 (
echo.
echo ERROR: C++ build failed.
exit /b 1
)

echo.
echo === Build complete ===
echo Output: build\lvt.exe
echo build\lvt_tap_x64.dll
echo Tests: build\lvt_unit_tests.exe
echo build\lvt_integration_tests.exe
19 changes: 19 additions & 0 deletions src/bounds_util.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#pragma once
#include <cmath>
#include <climits>
#include <optional>

namespace lvt {

// Safe double-to-int conversion: clamp to int range and reject non-finite values.
// Returns std::nullopt for NaN/Infinity so callers can skip bounds entirely.
// Prevents undefined behavior from static_cast<int> when the double is NaN, Inf,
// or outside the representable int range.
inline std::optional<int> safe_double_to_int(double v) {
if (!std::isfinite(v)) return std::nullopt;
if (v >= static_cast<double>(INT_MAX)) return INT_MAX;
if (v <= static_cast<double>(INT_MIN)) return INT_MIN;
return static_cast<int>(v);
}

} // namespace lvt
30 changes: 30 additions & 0 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include <cstring>
#include <string>
#include <fstream>
#include <nlohmann/json.hpp>

static void print_usage() {
fprintf(stderr,
Expand All @@ -30,6 +31,9 @@ static void print_usage() {
" --output <file> Write output to file instead of stdout\n"
" --format <fmt> Output format: json (default) or xml\n"
" --screenshot <file> Capture annotated screenshot to PNG\n"
#ifndef NDEBUG
" --annotations-json <file> Write annotation rectangles as JSON (test hook)\n"
Comment thread
asklar marked this conversation as resolved.
#endif
" --dump Output the tree (default; implied unless --screenshot)\n"
" --element <id> Scope to a specific element subtree\n"
" --frameworks Just detect and list frameworks\n"
Expand All @@ -47,6 +51,9 @@ struct Args {
std::string outputFile;
std::string format = "json";
std::string screenshotFile;
#ifndef NDEBUG
std::string annotationsFile;
#endif
std::string elementId;
int depth = -1;
bool frameworksOnly = false;
Expand Down Expand Up @@ -75,6 +82,10 @@ static Args parse_args(int argc, char* argv[]) {
args.format = argv[++i];
} else if (strcmp(argv[i], "--screenshot") == 0 && i + 1 < argc) {
args.screenshotFile = argv[++i];
#ifndef NDEBUG
} else if (strcmp(argv[i], "--annotations-json") == 0 && i + 1 < argc) {
args.annotationsFile = argv[++i];
#endif
} else if (strcmp(argv[i], "--element") == 0 && i + 1 < argc) {
args.elementId = argv[++i];
} else if (strcmp(argv[i], "--depth") == 0 && i + 1 < argc) {
Expand Down Expand Up @@ -265,6 +276,25 @@ int main(int argc, char* argv[]) {
}
}

#ifndef NDEBUG
// Annotations JSON — test hook for verifying which elements are annotated
if (!args.annotationsFile.empty()) {
auto annotations = lvt::collect_annotations(target.hwnd, &tree);
nlohmann::json aj = nlohmann::json::array();
for (auto& a : annotations) {
aj.push_back({{"id", a.id}, {"x", a.x}, {"y", a.y},
{"width", a.width}, {"height", a.height}});
}
std::ofstream out(args.annotationsFile);
if (out) {
out << aj.dump(2) << "\n";
if (lvt::g_debug)
fprintf(stderr, "lvt: wrote %zu annotations to %s\n",
annotations.size(), args.annotationsFile.c_str());
}
}
#endif

lvt::unload_plugins();
return 0;
}
20 changes: 14 additions & 6 deletions src/plugin_loader.cpp
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
#include "plugin_loader.h"
#include "debug.h"
#include "bounds_util.h"
#include <nlohmann/json.hpp>
#include <cstdio>
#include <cstdlib>
#include <cmath>
#include <functional>
#include <userenv.h>

Expand Down Expand Up @@ -143,13 +145,19 @@ static void graft_json_node(const json& j, Element& parent, const std::string& f
double oy = j.value("offsetY", 0.0);
double w = j.value("width", 0.0);
double h = j.value("height", 0.0);
double absX = parentOffsetX + ox;
double absY = parentOffsetY + oy;
double absX = std::isfinite(ox) ? parentOffsetX + ox : parentOffsetX;
double absY = std::isfinite(oy) ? parentOffsetY + oy : parentOffsetY;
if (w > 0 && h > 0) {
el.bounds.x = static_cast<int>(absX);
el.bounds.y = static_cast<int>(absY);
el.bounds.width = static_cast<int>(w);
el.bounds.height = static_cast<int>(h);
auto sx = safe_double_to_int(absX);
auto sy = safe_double_to_int(absY);
auto sw = safe_double_to_int(w);
auto sh = safe_double_to_int(h);
if (sx && sy && sw && sh) {
el.bounds.x = *sx;
el.bounds.y = *sy;
el.bounds.width = *sw;
el.bounds.height = *sh;
}
}

// Copy additional properties if present
Expand Down
16 changes: 12 additions & 4 deletions src/providers/wpf_inject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include "wpf_inject.h"
#include "../debug.h"
#include "../target.h"
#include "../bounds_util.h"

#include <Windows.h>
#include <objbase.h>
Expand All @@ -15,6 +16,7 @@
#include <wil/resource.h>
#include <nlohmann/json.hpp>
#include <cstdio>
#include <cmath>
#include <string>
#include <fstream>

Expand Down Expand Up @@ -71,10 +73,16 @@ static void graft_json_node(const json& j, Element& parent, const std::string& f
double ox = j.value("offsetX", 0.0);
double oy = j.value("offsetY", 0.0);
if (w > 0 && h > 0) {
el.bounds.x = static_cast<int>(ox);
el.bounds.y = static_cast<int>(oy);
el.bounds.width = static_cast<int>(w);
el.bounds.height = static_cast<int>(h);
auto sx = safe_double_to_int(ox);
auto sy = safe_double_to_int(oy);
auto sw = safe_double_to_int(w);
auto sh = safe_double_to_int(h);
if (sx && sy && sw && sh) {
el.bounds.x = *sx;
el.bounds.y = *sy;
el.bounds.width = *sw;
el.bounds.height = *sh;
}
}

// Visibility/enabled as properties
Expand Down
Loading
Loading