Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
45 changes: 25 additions & 20 deletions .github/workflows/dotnet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,28 +82,28 @@ jobs:
# An unrecognized platform must stop the run rather than drop the project. Dropping
# it would produce a smaller matrix that still reports success, which is the failure
# this design exists to remove.
unknown=$(echo "$projects" | jq -r '[.[] | select(.platform as $p | ["neutral","windows"] | index($p) | not) | .platform] | unique | join(", ")')
unknown=$(echo "$projects" | jq -r '[.[] | select(.platform as $p | ["neutral","windows","macos"] | index($p) | not) | .platform] | unique | join(", ")')
if [ -n "$unknown" ]; then
echo "::error::Cannot place test project(s) on a runner. Unhandled platform(s): $unknown"
echo "::error::macOS is currently excluded from the matrix, so an ios-tied test project has nowhere to run."
echo "::error::An ios-tied test project needs 'dotnet workload install ios', which this job does not run; the iOS workflow is where iOS builds happen."
exit 1
fi

# macOS is deliberately absent from this mapping. A macOS runner builds any project
# whose target frameworks are widened on that host, and in a repo with an iOS head that
# pulls in a target framework needing a workload this job does not install, so every
# macOS cell fails during its build. Restoring the workload on each cell is slow and
# macOS runner minutes are billed at a premium, so the platform is excluded until the
# underlying problem is fixed rather than papered over. An ios-tied test project now
# fails the guard above instead of silently finding no runner.
# macOS is back in this mapping. It was excluded because a macOS runner widened
# ImGui.App's target frameworks to include net10.0-ios, which needs a workload this job
# does not install, so every macOS cell failed during its build with NETSDK1147. That
# widening is now opt-in (IncludeIosTargets, set only by the iOS workflow), so a macOS
# cell builds exactly what Linux and Windows build. An ios-tied test project would still
# have nowhere to run and fails the guard above rather than silently disappearing.
matrix=$(echo "$projects" | jq -c '
{
include: [
.[]
| . as $p
| {
neutral: ["ubuntu-latest", "windows-latest"],
windows: ["windows-latest"]
neutral: ["ubuntu-latest", "windows-latest", "macos-latest"],
windows: ["windows-latest"],
macos: ["macos-latest"]
}[$p.platform][]
| {
os: .,
Expand Down Expand Up @@ -184,7 +184,9 @@ jobs:
#
# The five UI suites are effectively the whole cost of this job. They run in parallel, so the
# slowest of them sets the job's duration, while all nine other test projects finish in about
# thirty-four seconds combined. They are therefore run on Linux only:
# thirty-four seconds combined. They are therefore run on Linux only -- which is also what
# keeps macOS affordable now that it is back in the matrix, since macOS runner minutes are
# billed at roughly ten times Linux:
#
# * measured per assembly on Windows: 17m28s, 14m32s, 10m05s, 8m20s, 1m01s against ~34s for
# everything else, in a job whose test phase took 17m45s.
Expand All @@ -194,15 +196,18 @@ jobs:
# * Linux is the faster host for this work, so it keeps them.
#
# Only the UI test projects are excluded. The example applications they drive stay in the
# build on both platforms, so a change that breaks one still fails here.
# build on every platform, so a change that breaks one still fails here.
#
# The test is on Linux rather than against Windows, so a platform added to the matrix later
# gets the cheap treatment by default rather than silently inheriting the expensive one.
- name: Test
shell: bash
run: |
set -euo pipefail
if [ "${{ runner.os }}" = "Windows" ]; then
ktsubuild test all --workspace "$GITHUB_WORKSPACE" --verbose --exclude "**/*.UITests/*"
else
if [ "${{ runner.os }}" = "Linux" ]; then
ktsubuild test all --workspace "$GITHUB_WORKSPACE" --verbose
else
ktsubuild test all --workspace "$GITHUB_WORKSPACE" --verbose --exclude "**/*.UITests/*"
fi

- name: Upload Coverage
Expand Down Expand Up @@ -306,17 +311,17 @@ jobs:

# Each platform's artifact holds one coverage.xml, already merged across that platform's test
# projects by `test all`. The downloads must stay in their own per-artifact directories so
# both survive: flattened, one platform's report would overwrite the other's and the scanner
# would see a single platform's coverage as though it were the whole matrix's.
# they all survive: flattened, one platform's report would overwrite the others' and the
# scanner would see a single platform's coverage as though it were the whole matrix's.
- name: Download Coverage
if: needs.discover.outputs.has_tests == 'true'
uses: actions/download-artifact@v7
with:
pattern: coverage-*
path: coverage

# The UI suites are excluded from the Windows test job, so their coverage exists only in the
# Linux report, which records the paths that runner checked out to. This job analyses a
# The UI suites run on Linux only, so their coverage exists only in the Linux report, which
# records the paths that runner checked out to. This job analyses a
# Windows checkout, and Sonar matches coverage to source files by path, so every entry from
# that report was dropped without a word: five suites' worth of coverage, and with it every
# file only they exercise, reported as untested. Pointing those paths at this workspace is
Expand Down
14 changes: 14 additions & 0 deletions .github/workflows/ios.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,16 @@ jobs:
permissions:
contents: read

# This workflow is the only one that builds the iOS head, and the only one that installs the
# workload it needs. The csprojs default IncludeIosTargets to false, so a macOS host without
# that workload -- every macOS cell of the test matrix -- builds the same target frameworks as
# Linux and Windows instead of failing with NETSDK1147 (#327). MSBuild reads environment
# variables as properties, so setting it at job level opts every dotnet invocation in the job
# in, including the ones that reach ImGui.App through a ProjectReference and would otherwise
# resolve its net10.0 head rather than its net10.0-ios one.
env:
IncludeIosTargets: "true"

steps:
- name: Checkout Repository
uses: actions/checkout@v5
Expand Down Expand Up @@ -114,6 +124,10 @@ jobs:
permissions:
contents: read

# See the ios-build job: the iOS head is opt-in, and this is the other job that opts in.
env:
IncludeIosTargets: "true"

steps:
- name: Checkout Repository
uses: actions/checkout@v5
Expand Down
16 changes: 15 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# CLAUDE.md
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Expand Down Expand Up @@ -473,6 +473,20 @@ All C# files require this header:

## CI/CD

The iOS target framework is opt-in. `ImGui.App`, `ImGuiAppDemo.iOS` and `ImGui.App.iOS.SmokeTest`
add their `net10.0-ios` head only when `IncludeIosTargets` is `true` **and** the host is macOS;
otherwise `ImGui.App` cross-targets `net10.0;net9.0;net8.0` and the two app projects degrade to a
plain `net10.0` console exe. Only `.github/workflows/ios.yml` opts in, at job level, and it is the
only workflow that runs `dotnet workload install ios`. Without the gate, every macOS build widened
itself to `net10.0-ios` and failed with `NETSDK1147` before reaching a test, which is why macOS was
excluded from the test matrix (#327). If you add a step that has to build the iOS head, set
`IncludeIosTargets` — an environment variable works, MSBuild reads it as a property.

The test matrix in `dotnet.yml` fans out over Linux, Windows and macOS. The five UI suites run on
Linux only: they are the whole cost of the job, and the CPU rasterizer they drive measures the same
on either host. The `Test` step tests for Linux rather than against Windows, so any platform added
later gets that cheap treatment by default.

Uses `scripts/PSBuild.psm1` PowerShell module for CI pipeline. Version increments are controlled by commit message tags: `[major]`, `[minor]`, `[patch]`, `[pre]`. Auto-generated files (VERSION.md, CHANGELOG.md, LICENSE.md) should not be manually edited. CI runs on Windows, publishes to NuGet, uses SonarQube for analysis.

## Code Quality
Expand Down
11 changes: 10 additions & 1 deletion ImGui.App.Testing/ImGuiAppHarness.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) 2023-2026 ktsu-dev contributors
// Copyright (c) 2023-2026 ktsu-dev contributors

namespace ktsu.ImGui.App.Testing;

Expand Down Expand Up @@ -100,6 +100,15 @@
Hexa.NET.ImGui.ImGui.GetIO().ConfigFlags |= Hexa.NET.ImGui.ImGuiConfigFlags.DockingEnable;
}

// Dear ImGui defaults ConfigMacOSXBehaviors to true on Apple platforms, and one of those
// behaviours swaps Ctrl and Super so that Cmd drives shortcuts. An injected ImGuiKey.ModCtrl
// therefore reaches the application as KeySuper on macOS and as KeyCtrl everywhere else, so
// the same test asserts different things depending on the host. The harness exists to inject
// input deterministically, so it pins the behaviour off and Ctrl means Ctrl on every
// platform. A test that wants the macOS mapping can set the flag itself; it is read afresh
// each frame.
Hexa.NET.ImGui.ImGui.GetIO().ConfigMacOSXBehaviors = false;

// ImGuiController does this for a windowed application, and the harness replaces the
// controller. Without it ImGuizmo, ImNodes and ImPlot never learn the ImGui context and an
// application that draws with any of them faults inside native code rather than failing as
Expand Down Expand Up @@ -262,7 +271,7 @@

if (ReferenceEquals(live, this))
{
live = null;

Check warning on line 274 in ImGui.App.Testing/ImGuiAppHarness.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Remove this set, which updates a 'static' field from an instance method.

Check warning on line 274 in ImGui.App.Testing/ImGuiAppHarness.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Remove this set, which updates a 'static' field from an instance method.

Check warning on line 274 in ImGui.App.Testing/ImGuiAppHarness.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Remove this set, which updates a 'static' field from an instance method.

Check warning on line 274 in ImGui.App.Testing/ImGuiAppHarness.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Remove this set, which updates a 'static' field from an instance method.
ImGuiProbes.SetProbe(null);
ImGuiApp.EndExternalFrameSession();
}
Expand Down
11 changes: 9 additions & 2 deletions ImGui.App/ImGui.App.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project>
<Project>
<Sdk Name="Microsoft.NET.Sdk" />
<Sdk Name="ktsu.Sdk" />

Expand All @@ -8,8 +8,15 @@

<PropertyGroup>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>

<!-- The iOS head is opt-in rather than implied by the host OS. Building net10.0-ios needs
`dotnet workload install ios`, which only the iOS workflow installs, so widening the
target frameworks on every macOS host made ordinary macOS builds fail with NETSDK1147
before they reached a single test. Only the iOS workflow sets IncludeIosTargets=true. -->
<IncludeIosTargets Condition="'$(IncludeIosTargets)' == ''">false</IncludeIosTargets>

<TargetFrameworks>net10.0;net9.0;net8.0</TargetFrameworks>
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('OSX'))">$(TargetFrameworks);net10.0-ios</TargetFrameworks>
<TargetFrameworks Condition="'$(IncludeIosTargets)' == 'true' and $([MSBuild]::IsOSPlatform('OSX'))">$(TargetFrameworks);net10.0-ios</TargetFrameworks>
</PropertyGroup>

<PropertyGroup Condition="$(TargetFramework.Contains('-ios'))">
Expand Down
14 changes: 8 additions & 6 deletions examples/ImGuiAppDemo.iOS/ImGuiAppDemo.iOS.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
Expand All @@ -9,13 +9,15 @@
<RootNamespace>ktsu.ImGui.Examples.App.iOS</RootNamespace>
<AssemblyName>ImGuiAppDemoiOS</AssemblyName>

<!-- Like the smoke app: this is an iOS application only on macOS (where the iOS workload exists).
On any other OS it degrades to a plain net10.0 console exe so the Windows CI pipeline can still
restore/build it without the iOS workload. The simulator job always builds -f net10.0-ios.
<!-- This project is an iOS application only when the iOS workload is present, which the iOS
workflow guarantees by setting IncludeIosTargets=true. Everywhere else — including an
ordinary macOS build — it degrades to a plain net10.0 console exe, so a host without the
workload can still restore and build it. The simulator job always builds -f net10.0-ios.
We deliberately use the plain Microsoft.NET.Sdk (not ktsu.Sdk.App) because the desktop-app SDK
forces a RID matrix that fights the iOS build; the smoke app proved this minimal SDK works. -->
<TargetFramework Condition="$([MSBuild]::IsOSPlatform('OSX'))">net10.0-ios</TargetFramework>
<TargetFramework Condition="!$([MSBuild]::IsOSPlatform('OSX'))">net10.0</TargetFramework>
<IncludeIosTargets Condition="'$(IncludeIosTargets)' == ''">false</IncludeIosTargets>
<TargetFramework Condition="'$(IncludeIosTargets)' == 'true' and $([MSBuild]::IsOSPlatform('OSX'))">net10.0-ios</TargetFramework>
<TargetFramework Condition="'$(TargetFramework)' == ''">net10.0</TargetFramework>
</PropertyGroup>

<PropertyGroup Condition="$(TargetFramework.Contains('-ios'))">
Expand Down
22 changes: 21 additions & 1 deletion tests/ImGui.App.Testing.Tests/ImGuiAppHarnessTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) 2023-2026 ktsu-dev contributors
// Copyright (c) 2023-2026 ktsu-dev contributors

namespace ktsu.ImGui.App.Testing.Tests;

Expand Down Expand Up @@ -246,6 +246,26 @@ public void Keyboard_PressWithCtrl_ReportsTheModifier()
Assert.IsTrue(sawCtrlZ, "Ctrl and Z should arrive together, which is what a shortcut needs.");
}

/// <summary>
/// Regression test for the macOS Ctrl/Super swap. Dear ImGui defaults
/// <c>ConfigMacOSXBehaviors</c> to true on Apple platforms, which swaps Ctrl and Super so Cmd
/// drives shortcuts; an injected <see cref="ImGuiKey.ModCtrl"/> then arrives as
/// <c>KeySuper</c>. The harness pins the behaviour off, so this asserts the modifier lands
/// where the caller asked and nowhere else.
/// </summary>
[TestMethod]
public void Keyboard_PressWithCtrl_DoesNotArriveAsSuper()
{
bool sawSuper = false;
using ImGuiAppHarness harness = ImGuiAppHarness.Start(
new ImGuiAppConfig { OnRender = _ => sawSuper |= ImGui.GetIO().KeySuper },
Window());

harness.Keyboard.Press(ImGuiKey.Z, ctrl: true);

Assert.IsFalse(sawSuper, "Ctrl must not be remapped to Super, which is what macOS behaviours would do.");
}

[TestMethod]
public void Keyboard_Type_DeliversEveryCharacterInOrder()
{
Expand Down
14 changes: 8 additions & 6 deletions tests/ImGui.App.iOS.SmokeTest/ImGui.App.iOS.SmokeTest.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
Expand All @@ -9,11 +9,13 @@
<RootNamespace>ktsu.ImGui.App.iOS.SmokeTest</RootNamespace>
<AssemblyName>ImGuiAppiOSSmokeTest</AssemblyName>

<!-- This project is an iOS application only on macOS (where the iOS workload exists). On any
other OS it degrades to a plain net10.0 console exe so the Windows CI pipeline can still
restore/build it without the iOS workload. The simulator job always builds -f net10.0-ios. -->
<TargetFramework Condition="$([MSBuild]::IsOSPlatform('OSX'))">net10.0-ios</TargetFramework>
<TargetFramework Condition="!$([MSBuild]::IsOSPlatform('OSX'))">net10.0</TargetFramework>
<!-- This project is an iOS application only when the iOS workload is present, which the iOS
workflow guarantees by setting IncludeIosTargets=true. Everywhere else — including an
ordinary macOS build — it degrades to a plain net10.0 console exe, so a host without the
workload can still restore and build it. The simulator job always builds -f net10.0-ios. -->
<IncludeIosTargets Condition="'$(IncludeIosTargets)' == ''">false</IncludeIosTargets>
<TargetFramework Condition="'$(IncludeIosTargets)' == 'true' and $([MSBuild]::IsOSPlatform('OSX'))">net10.0-ios</TargetFramework>
<TargetFramework Condition="'$(TargetFramework)' == ''">net10.0</TargetFramework>
</PropertyGroup>

<PropertyGroup Condition="$(TargetFramework.Contains('-ios'))">
Expand Down