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
3 changes: 3 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ updates:
# MauiVersion (Directory.Packages.props) is the deliberate minimum KumikoUI.Maui supports,
# not a version to track: raising it forces every consumer onto that MAUI. Raise it by hand.
- dependency-name: "Microsoft.Maui.Controls"
# The [KumikoGridItem] generator is built against the oldest Roslyn it supports; a newer
# Microsoft.CodeAnalysis.CSharp would stop older compilers from loading it. Raise it by hand.
- dependency-name: "Microsoft.CodeAnalysis.CSharp"
groups:
# One major version across the whole repository (see Directory.Packages.props).
skiasharp:
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ jobs:
run: dotnet restore src/KumikoUI.Core/KumikoUI.Core.csproj
&& dotnet restore src/KumikoUI.SkiaSharp/KumikoUI.SkiaSharp.csproj
&& dotnet restore tests/KumikoUI.Core.Tests/KumikoUI.Core.Tests.csproj
&& dotnet restore tests/KumikoUI.SourceGenerators.Tests/KumikoUI.SourceGenerators.Tests.csproj

- name: Build — Core
run: dotnet build src/KumikoUI.Core/KumikoUI.Core.csproj
Expand All @@ -55,6 +56,10 @@ jobs:
run: dotnet test tests/KumikoUI.Core.Tests/KumikoUI.Core.Tests.csproj
--no-restore -c Release --logger "trx" --results-directory TestResults

- name: Test — source generator
run: dotnet test tests/KumikoUI.SourceGenerators.Tests/KumikoUI.SourceGenerators.Tests.csproj
--no-restore -c Release --logger "trx" --results-directory TestResults

- name: Upload test results
if: always()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,9 @@ jobs:
dotnet restore tests/KumikoUI.Core.Tests/KumikoUI.Core.Tests.csproj
dotnet test tests/KumikoUI.Core.Tests/KumikoUI.Core.Tests.csproj \
-c Release --logger "trx" --results-directory TestResults
dotnet restore tests/KumikoUI.SourceGenerators.Tests/KumikoUI.SourceGenerators.Tests.csproj
dotnet test tests/KumikoUI.SourceGenerators.Tests/KumikoUI.SourceGenerators.Tests.csproj \
-c Release --logger "trx" --results-directory TestResults

test-uno:
name: Run Uno tests
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,6 @@ artifacts/
Thumbs.db

*.lscache

# BenchmarkDotNet output (bench/)
BenchmarkDotNet.Artifacts/
12 changes: 11 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
## [Unreleased]

### Added
- Native AOT and trimming support (#17). Item properties are read and written, in order, through:
- `KumikoColumn.ValueAccessor` / `ValueSetter` delegates (and `EditorDisplaySelector` / `EditorValueSelector` for ComboBox items, `DrawnComboBox.DisplaySelector` / `ValueSelector`);
- accessors generated for types marked `[KumikoGridItem]` (`KumikoUI.Core.Data`), by a source generator now included in the KumikoUI.Core package;
- providers registered with `KumikoAccessorRegistry.Register` / `AddProvider` (`IKumikoAccessorProvider`, `KumikoPropertyAccessor`);
- reflection, as before.

Generated and delegate reads are 3–5× faster than the previous compiled expression trees, with 80% less allocation (`bench/KumikoUI.Benchmarks`). All four packages are marked `IsAotCompatible` and build with no trimming or AOT analyzer warnings; such warnings now fail the build.
- **KumikoUI.Uno**: a new package hosting the grid on the Uno Platform (Windows App SDK, desktop on Windows/macOS/Linux, WebAssembly, Android, iOS) with the same engine, rendering and features as KumikoUI.Maui, plus `KumikoFonts` for registering custom fonts from app assets, and the `SampleApp.Uno` sample.
- `DataGridStyle.PinBottomSummaryRows` (default `true`): `Position=Bottom` table summary rows are pinned to the bottom of the viewport as a fixed footer instead of scrolling after the last row. **Behaviour change** for existing users of bottom summaries; set it to `false` to restore the previous layout.
- `KumikoStyle.PinBottomSummaryRows` (default `true`): `Position=Bottom` table summary rows are pinned to the bottom of the viewport as a fixed footer instead of scrolling after the last row. **Behaviour change** for existing users of bottom summaries; set it to `false` to restore the previous layout.

### Changed
- The reflection fallback for item properties is off for `PublishAot=true` and `TrimMode=full` publishes (KumikoUI.Core build targets set the `KumikoUI.Core.IsReflectionPropertyAccessSupported` switch). A property with no generated accessor, delegate or provider then reads as null and is traced once. Override with the `KumikoReflectionPropertyAccess` MSBuild property. Partial trimming, the .NET MAUI default, is unaffected.
- A nested property path with a `null` value part-way (`Address.City` when `Address` is null) now reads as an empty cell instead of throwing, and an edit to it is ignored.
- **Breaking:** naming pass: every grid type now uses the `Kumiko` prefix (drawn components keep `Drawn`), and no public name collides with types of MAUI, CommunityToolkit.Maui, WinUI/Uno, the Windows Community Toolkit DataGrid, WPF, Windows Forms or Avalonia (#16). Namespaces are unchanged (`KumikoUI.*`); member names are unchanged (for example `KumikoColumn.SortDirection`, `KumikoDataGrid.GridSelectionMode` and `KumikoDataGrid.GridStyle` keep their names, only their types change). Update XAML and code as follows:
- Host control: `DataGridView` → `KumikoDataGrid` in both `KumikoUI.Maui` and `KumikoUI.Uno`, including XAML (`<dg:DataGridView …>` → `<dg:KumikoDataGrid …>`, `<dg:DataGridView.Columns>` → `<dg:KumikoDataGrid.Columns>`, and the same for `TableSummaryRows` and `GroupSummaryRows`).
- Models: `DataGridSource` → `KumikoSource`, `DataGridColumn` → `KumikoColumn` (including `<core:DataGridColumn …>` in XAML), `DataGridColumnType` → `KumikoColumnType`, `DataGridStyle` → `KumikoStyle`, `DataGridTheme` → `KumikoTheme`, `DataGridThemeMode` → `KumikoThemeMode`, `CellStyle` → `KumikoCellStyle`, `RowStyle` → `KumikoRowStyle`, `DataGridRenderer` → `KumikoRenderer`
Expand All @@ -36,6 +45,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
- KumikoUI.Uno is built with Uno.Sdk 6.7.30 (was 6.5.31).

### Fixed
- ComboBox editors now apply the column's `EditorDisplayMemberPath` and `EditorValueMemberPath`; they were set after the items were built, so items showed `ToString()` and the current value was not selected.
- An unknown font family now falls back to the default family in the requested weight and slant instead of producing a null typeface. Styled typefaces are resolved through `SKFontManager` with an explicit family, which SkiaSharp 4 needs to keep bold and italic.
- Taps on the pinned summary footer no longer select or edit the data row behind it; unpinned bottom summary rows are also reported as `HitRegion.TableSummaryRow`.
- Keyboard navigation keeps the current row fully visible between the header/frozen rows and the pinned footer, and accounts for the grouping panel and top summary rows (previously the scroll offset was counted twice).
Expand Down
3 changes: 3 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,11 @@ Build what you are working on; the platform-agnostic projects need no workloads:
dotnet build src/KumikoUI.Core
dotnet build src/KumikoUI.SkiaSharp
dotnet test tests/KumikoUI.Core.Tests
dotnet test tests/KumikoUI.SourceGenerators.Tests
```

The libraries are `IsAotCompatible`: a trimming or Native AOT analyzer warning (IL2xxx/IL3xxx) fails the build (`Directory.Build.targets`). Read item properties through `KumikoAccessorRegistry` rather than reflection (see "Native AOT & Trimming" in the README). Property-access benchmarks live in `bench/` (see `bench/README.md`).

Check public type names after building (see [Public type names](#public-type-names)):

```bash
Expand Down
15 changes: 15 additions & 0 deletions Directory.Build.targets
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<Project>
<!--
Libraries marked IsAotCompatible (KumikoUI.Core, .SkiaSharp, .Maui, .Uno) must stay free of trimming,
Native AOT and single-file analyzer warnings (#17).
- EnableTrimAnalyzer is forced on because the Apple workloads turn it off for library builds.
- Every IL2xxx/IL3xxx analyzer warning is an error, so a regression fails the build instead of
surfacing as a warning in apps that publish with trimming or Native AOT.
-->
<PropertyGroup Condition="'$(IsAotCompatible)' == 'true'">
<EnableTrimAnalyzer>true</EnableTrimAnalyzer>
<EnableAotAnalyzer>true</EnableAotAnalyzer>
<EnableSingleFileAnalyzer>true</EnableSingleFileAnalyzer>
<WarningsAsErrors>$(WarningsAsErrors);IL2001;IL2002;IL2003;IL2004;IL2005;IL2006;IL2007;IL2008;IL2009;IL2010;IL2011;IL2012;IL2013;IL2014;IL2015;IL2016;IL2017;IL2018;IL2019;IL2020;IL2021;IL2022;IL2023;IL2024;IL2025;IL2026;IL2027;IL2028;IL2029;IL2030;IL2031;IL2032;IL2033;IL2034;IL2035;IL2036;IL2037;IL2038;IL2039;IL2040;IL2041;IL2042;IL2043;IL2044;IL2045;IL2046;IL2047;IL2048;IL2049;IL2050;IL2051;IL2052;IL2053;IL2054;IL2055;IL2056;IL2057;IL2058;IL2059;IL2060;IL2061;IL2062;IL2063;IL2064;IL2065;IL2066;IL2067;IL2068;IL2069;IL2070;IL2071;IL2072;IL2073;IL2074;IL2075;IL2076;IL2077;IL2078;IL2079;IL2080;IL2081;IL2082;IL2083;IL2084;IL2085;IL2086;IL2087;IL2088;IL2089;IL2090;IL2091;IL2092;IL2093;IL2094;IL2095;IL2096;IL2097;IL2098;IL2099;IL2100;IL2101;IL2102;IL2103;IL2104;IL2105;IL2106;IL2107;IL2108;IL2109;IL2110;IL2111;IL2112;IL2113;IL2114;IL2115;IL2116;IL2117;IL2118;IL2119;IL2120;IL2121;IL2122;IL2123;IL3000;IL3001;IL3002;IL3003;IL3004;IL3005;IL3050;IL3051;IL3052;IL3053;IL3054;IL3055;IL3056;IL3057;IL3058</WarningsAsErrors>
</PropertyGroup>
</Project>
12 changes: 12 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,18 @@
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="$(AvaloniaDataGridVersion)" />
</ItemGroup>

<!--
Roslyn for the [KumikoGridItem] source generator: the oldest compiler the generator supports. Apps on
.NET 10 compile with a newer Roslyn, which loads generators built against older ones.
-->
<ItemGroup Label="Source generator">
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
</ItemGroup>

<ItemGroup Label="Benchmarks">
<PackageVersion Include="BenchmarkDotNet" Version="0.15.8" />
</ItemGroup>

<ItemGroup Label="Tests">
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.11.0" />
<PackageVersion Include="xunit" Version="2.9.3" />
Expand Down
30 changes: 30 additions & 0 deletions KumikoUI.sln
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Build", "Build", "{DFDD4E5D
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KumikoUI.Uno.Tests", "tests\KumikoUI.Uno.Tests\KumikoUI.Uno.Tests.csproj", "{57855B3A-D369-4D2D-8B43-FA09549FF955}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KumikoUI.SourceGenerators", "src\KumikoUI.SourceGenerators\KumikoUI.SourceGenerators.csproj", "{EA495C23-8110-4B63-A97D-A8C68483F9B4}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KumikoUI.SourceGenerators.Tests", "tests\KumikoUI.SourceGenerators.Tests\KumikoUI.SourceGenerators.Tests.csproj", "{A0D6A010-D7CD-4DFE-A011-3413544A2C9A}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -144,6 +148,30 @@ Global
{57855B3A-D369-4D2D-8B43-FA09549FF955}.Release|x64.Build.0 = Release|Any CPU
{57855B3A-D369-4D2D-8B43-FA09549FF955}.Release|x86.ActiveCfg = Release|Any CPU
{57855B3A-D369-4D2D-8B43-FA09549FF955}.Release|x86.Build.0 = Release|Any CPU
{EA495C23-8110-4B63-A97D-A8C68483F9B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EA495C23-8110-4B63-A97D-A8C68483F9B4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EA495C23-8110-4B63-A97D-A8C68483F9B4}.Debug|x64.ActiveCfg = Debug|Any CPU
{EA495C23-8110-4B63-A97D-A8C68483F9B4}.Debug|x64.Build.0 = Debug|Any CPU
{EA495C23-8110-4B63-A97D-A8C68483F9B4}.Debug|x86.ActiveCfg = Debug|Any CPU
{EA495C23-8110-4B63-A97D-A8C68483F9B4}.Debug|x86.Build.0 = Debug|Any CPU
{EA495C23-8110-4B63-A97D-A8C68483F9B4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EA495C23-8110-4B63-A97D-A8C68483F9B4}.Release|Any CPU.Build.0 = Release|Any CPU
{EA495C23-8110-4B63-A97D-A8C68483F9B4}.Release|x64.ActiveCfg = Release|Any CPU
{EA495C23-8110-4B63-A97D-A8C68483F9B4}.Release|x64.Build.0 = Release|Any CPU
{EA495C23-8110-4B63-A97D-A8C68483F9B4}.Release|x86.ActiveCfg = Release|Any CPU
{EA495C23-8110-4B63-A97D-A8C68483F9B4}.Release|x86.Build.0 = Release|Any CPU
{A0D6A010-D7CD-4DFE-A011-3413544A2C9A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A0D6A010-D7CD-4DFE-A011-3413544A2C9A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A0D6A010-D7CD-4DFE-A011-3413544A2C9A}.Debug|x64.ActiveCfg = Debug|Any CPU
{A0D6A010-D7CD-4DFE-A011-3413544A2C9A}.Debug|x64.Build.0 = Debug|Any CPU
{A0D6A010-D7CD-4DFE-A011-3413544A2C9A}.Debug|x86.ActiveCfg = Debug|Any CPU
{A0D6A010-D7CD-4DFE-A011-3413544A2C9A}.Debug|x86.Build.0 = Debug|Any CPU
{A0D6A010-D7CD-4DFE-A011-3413544A2C9A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A0D6A010-D7CD-4DFE-A011-3413544A2C9A}.Release|Any CPU.Build.0 = Release|Any CPU
{A0D6A010-D7CD-4DFE-A011-3413544A2C9A}.Release|x64.ActiveCfg = Release|Any CPU
{A0D6A010-D7CD-4DFE-A011-3413544A2C9A}.Release|x64.Build.0 = Release|Any CPU
{A0D6A010-D7CD-4DFE-A011-3413544A2C9A}.Release|x86.ActiveCfg = Release|Any CPU
{A0D6A010-D7CD-4DFE-A011-3413544A2C9A}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -158,5 +186,7 @@ Global
{5565DAEA-A170-462E-B2F5-0BDD6878319C} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
{DFDD4E5D-1418-41C2-A6CE-2A6397026499} = {17B595E7-34C1-4B92-9FD3-316E5A57E051}
{57855B3A-D369-4D2D-8B43-FA09549FF955} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
{EA495C23-8110-4B63-A97D-A8C68483F9B4} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{A0D6A010-D7CD-4DFE-A011-3413544A2C9A} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
EndGlobalSection
EndGlobal
46 changes: 45 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ Every visual element — cells, headers, editors, scrollbars, popups — is rend
- [Custom Fonts](#-custom-fonts)
- [Scrolling & Performance](#-scrolling--performance)
- [Live Data](#-live-data)
- [Native AOT & Trimming](#-native-aot--trimming)
- [Architecture](#-architecture)
- [Sample Application](#-sample-application)
- [Requirements](#-requirements)
Expand Down Expand Up @@ -88,6 +89,7 @@ Every visual element — cells, headers, editors, scrollbars, popups — is rend
| **Theming** | Built-in Light / Dark / HighContrast; fully customisable style object |
| **Momentum scrolling** | Physics-based inertial scroll with configurable friction |
| **Live data** | `INotifyPropertyChanged` and `ObservableCollection` O(1) updates |
| **Native AOT & trimming** | Warning-free under `PublishAot`; `[KumikoGridItem]` source-generated accessors or per-column delegates |
| **Column resizing** | Drag column border |

---
Expand Down Expand Up @@ -815,7 +817,49 @@ public class Employee : INotifyPropertyChanged
<core:KumikoColumn Header="City" PropertyName="Address.City" Width="120" />
```

Accessors are compiled once via `Expression.Property` chains and cached.
Each path segment is resolved once per item type and cached. A `null` value part-way along the path reads as an empty cell, and an edit to it is ignored. How the properties are read is covered in [Native AOT & Trimming](#-native-aot--trimming).

---

## 🧩 Native AOT & Trimming

All KumikoUI packages are marked `IsAotCompatible` and build with no trimming or Native AOT warnings. The grid reads and writes item properties named by `PropertyName` through, in order:

1. **Column delegates**: `ValueAccessor` and `ValueSetter` on `KumikoColumn` (and `EditorDisplaySelector` / `EditorValueSelector` for ComboBox items).
2. **Generated accessors**: types marked `[KumikoGridItem]`. The `KumikoUI.Core` package includes a source generator that writes typed accessors for their public properties and registers them when the assembly loads.
3. **Registered providers**: `KumikoAccessorRegistry.Register(type, provider)` or `AddProvider(provider)` with your own `IKumikoAccessorProvider`, for example to read dictionary entries or data rows.
4. **Reflection**: everything else. This fallback is turned off automatically for `PublishAot=true` and `TrimMode=full`, because those publishes remove the metadata it reads. Partial trimming (the .NET MAUI default) keeps it on.

With reflection off, a property that no tier can serve reads as `null`, edits to it are ignored, and a warning naming the type and property is traced once.

```csharp
using KumikoUI.Core.Data;

[KumikoGridItem] // generated accessors for Name, Age, Address, …
public class Employee
{
public string Name { get; set; } = "";
public int Age { get; set; }
public Address? Address { get; set; }
}

[KumikoGridItem] // mark every type a path walks through ("Address.City")
public class Address
{
public string City { get; set; } = "";
}

// Or per column, without attributes:
var bonus = new KumikoColumn
{
Header = "Bonus",
PropertyName = "Bonus", // still names the column for summaries and grouping
ValueAccessor = item => ((Employee)item).Age * 10m,
ValueSetter = (item, value) => { /* write it back */ },
};
```

Generated accessors and delegates are faster than the expression trees the grid compiled before, and need no runtime code generation. `bench/KumikoUI.Benchmarks` measures them. The generator reports `KUMIKO001` for generic types and `KUMIKO002` for types it cannot reference (private, protected or file-local); use delegates or a provider for those. Set the `KumikoReflectionPropertyAccess` MSBuild property to `true` or `false` to override the automatic choice.

---

Expand Down
25 changes: 25 additions & 0 deletions bench/KumikoUI.Benchmarks/KumikoUI.Benchmarks.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">

<!-- Property-access benchmarks for #17, not run in CI. See bench/README.md for how to run them. -->
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IncludeSymbols>false</IncludeSymbols>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="BenchmarkDotNet" />
<PackageReference Remove="MinVer" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="../../src/KumikoUI.Core/KumikoUI.Core.csproj" />
<ProjectReference Include="../../src/KumikoUI.SourceGenerators/KumikoUI.SourceGenerators.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
</ItemGroup>

</Project>
3 changes: 3 additions & 0 deletions bench/KumikoUI.Benchmarks/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
using BenchmarkDotNet.Running;

BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);
Loading
Loading