Skip to content

Commit 549fcd1

Browse files
committed
chore(spike): keep row cells out of the ScrollViewer's anchor candidates [grid-only]
1 parent d720022 commit 549fcd1

5 files changed

Lines changed: 92 additions & 7 deletions

File tree

‎.github/workflows/spike-wingates2.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ jobs:
3838

3939
ime:
4040
name: IME harness (${{ matrix.arch }})
41+
if: ${{ !contains(github.event.head_commit.message, '[grid-only]') }}
4142
strategy:
4243
fail-fast: false
4344
matrix:

‎spikes/wingates2/SPIKE.md‎

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# Windows gates 2: ItemsRepeater grid and IME harness
2+
3+
Run on 2026-11-02, lane `wingates2`, on GitHub-hosted runners. Branch `spike/wingates2`.
4+
5+
Scope changed mid-run: the maintainer dropped the WinUIEdit fork (the SQL editor will be our own control). Gate B became an IME and UIA harness for that future control. The fork's build numbers are kept at the end as a baseline.
6+
7+
GRID_SUMMARY
8+
9+
## Setup
10+
11+
- Runners, from the job logs:
12+
- Arm64: `windows-11-arm`, image `windows-11-vs2026-arm64` 20261025.196.1, Windows 11 Enterprise 10.0.26200.
13+
- x64: `windows-2025`, image `windows-2025-vs2026` 20261021.278.1, Windows Server 2025 10.0.26100.
14+
- Both: 16 GB (probe, run 41425279462), Visual Studio 2026 18.11, .NET SDK 10.0.403 (Arm64) and 10.0.402 (x64), rustc 1.98.1 from the repo's `rust-toolchain.toml`.
15+
- The runners have no GPU. WinUI renders with WARP on the CPU.
16+
- The session is interactive: `runneradmin` on the console session, with explorer, dwm and ctfmon running (probe, run 41425279462).
17+
- The display starts at 1024 x 768. `ci/desktop.ps1` sets 1920 x 1080 with `ChangeDisplaySettings` (`before=1024x768 change=0 after=1920x1080` in every build JSON). The window is 1440 x 900 at raster scale 1, so the viewport is 1424 x 826 DIPs, close to the previous run's 1440 x 900.
18+
- The Arm64 image signs in to "Choose privacy settings for your device" (`screenshots/arm64-first-logon-privacy-page.png`). It covers the desktop and keeps the foreground, so every key the harness sent went to it.
19+
- `ci/desktop.ps1` sets the documented policy `DisablePrivacyExperience` and stops `WWAHost`, the page's host (`foreground before: WWAHost (9104); stopping WWAHost (9104)`, run 41430925394).
20+
- SearchHost then holds the foreground. The apps take it back by calling `SetForegroundWindow` with Alt held, then `AttachThreadInput`, then by clicking their own status line.
21+
- Do not kill whatever owns the foreground. On x64 that is the runner's own console, and doing so ended the job (run 41430925394, job 125229000014: "The runner has received a shutdown signal").
22+
- Build, per job:
23+
- `griddata-core/`: the previous run's core with the wide fetch, built for the runner's target with `+crt-static`.
24+
- `app/`: `dotnet publish`, net10.0-windows10.0.26100.0, unpackaged, self-contained, `Microsoft.WindowsAppSDK.WinUI` 2.6.4.
25+
- `tool/` and `ime/driver/`: FlaUI.UIA3 5.0.0, out of process.
26+
- Measures are the previous run's, so the numbers compare:
27+
- `step_ms`: the scroll call plus `UpdateLayout()`.
28+
- `heartbeat_wait_ms`: how long a work item queued every 5 ms waits for the UI thread. This is the UI-thread block.
29+
- `frame_delta_ms`: time between `CompositionTarget.Rendering` ticks. WARP makes it pessimistic.
30+
- New this run: `measure_ms`, `get_element_ms` and `element_measure_ms` time the layout's own work, and `data_ms` and `bind_ms` time fetching and setting the text.
31+
- New this run: the grid is checked twice per jump, right after the step and one frame later. Every realized cell must hold its own row's text and sit at its own position (through `TransformToVisual`), and every visible cell must be realized.
32+
- The ListView baseline from the previous run (20 columns, a TextBlock per column, `ContainerContentChanging`) runs on the same runner in the same job.
33+
34+
GRID_SECTION
35+
36+
IME_SECTION
37+
38+
## WinUIEdit fork: reference baseline only
39+
40+
Dropped by the maintainer. No editor run happened. What exists:
41+
42+
- WinUIEdit `d9f60dbca65c` (0.0.5-prerelease, Scintilla 5.6.3, Lexilla 5.5.0) does not build as published with VS 2026, which the runners now carry:
43+
- The project pins toolset v143 (`MSB8020 ... build tools for 'v143' ... cannot be found`, run 41425279462).
44+
- With v145, `<experimental/coroutine>` stops the build (`error STL1011 ... /await ... will be REMOVED SOON`, run 41427138979), because the project builds C++17 with `/await` on C++/WinRT 2.0.
45+
- The vcxproj imported C++/WinRT 2.0.240405.15 while `packages.config` restored 2.0.250303.1.
46+
- With those three fixed, it built (run 41428403785, jobs 125220549029 and 125220549366):
47+
- Arm64: msbuild 146.3 s on the native arm64 MSBuild, `WinUIEditor.dll` 3,829,760 bytes.
48+
- x64: 167.4 s, 3,066,368 bytes.
49+
- Imports: UCRT API sets, USER32, KERNEL32, OLEAUT32, d3d11. No VCRUNTIME140.
50+
- Wrapper size: 56 files and 25,209 lines in `WinUIEditor/`, of which 22,438 are the generated `EditorWrapper.*`. The WinUI platform layer in `scintilla/winui` is 9 files and 5,469 lines.
51+
- Issue #26 (Chinese IME candidate window misaligned): the code shows the cause. `ScintillaWinUI::GetTextExt` and `GetScreenExt` return control coordinates where TSF wants screen coordinates (`// Todo: Translate to screen. DPI?` in `scintilla/winui/ScintillaWin.cxx`). Whether it reproduces was not run.
52+
- `results/winuiedit-build-baseline.txt` has the numbers.
53+
54+
## Files
55+
56+
- `app/`: the WinUI app. `grid` runs the grid gate and `listview` the baseline.
57+
- `GridGate/DataGridView.cs`: the control.
58+
- `GridGate/RowLayouts.cs`: the row-presenter layouts. `GridGate/CellLayout.cs`: the 2D layout and the header layout.
59+
- `GridGate/GridCell.cs`: the cell and the automation peers.
60+
- `GridGate/GridData.cs`: packed windows from the core. `GridGate/GridBench.cs`: the bench.
61+
- `Common.cs`: frame loop, heartbeat, `SendInput`, screen capture.
62+
- `tool/`: the UIA driver for the grid.
63+
- `ime/`: the reusable IME harness. See IME_FILES.
64+
- `griddata-core/`: the Rust core.
65+
- `ci/`: `run-grid.ps1` and `desktop.ps1` (display size, first-logon page).
66+
- `results/`: raw JSON of the runs quoted here. `screenshots/`.
67+
- `../../.github/workflows/spike-wingates2.yml`: the temporary workflow.

‎spikes/wingates2/app/GridGate/GridBench.cs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,9 @@ private static async Task SelectionPhase(RunContext ctx, DataGridView grid, Grid
307307

308308
var (sx, sy) = CellCenterOnScreen(ctx, grid, row + 3, col + 2);
309309
Native.Down(Native.VkShift);
310+
await Task.Delay(150);
310311
Native.Click(sx, sy);
312+
await Task.Delay(150);
311313
Native.Up(Native.VkShift);
312314
await Task.Delay(400);
313315
bool range = grid.Selection == (row, col, row + 3, col + 2);

‎spikes/wingates2/app/GridGate/RowLayouts.cs‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,10 @@ protected override Size ArrangeOverride(VirtualizingLayoutContext context, Size
138138
}
139139

140140
// A realized row. A recycled presenter keeps its cells; moving it to another row only rebinds their text.
141-
internal sealed partial class RowPresenter : Microsoft.UI.Xaml.Controls.Grid
141+
// It is the IScrollAnchorProvider its cell repeater finds first, so cells never become anchor candidates of the
142+
// grid's ScrollViewer: a presenter moved to another row would carry them along, and the ScrollViewer would shift
143+
// its offset to follow them. The rows themselves stay anchor candidates through the outer repeater.
144+
internal sealed partial class RowPresenter : Microsoft.UI.Xaml.Controls.Grid, IScrollAnchorProvider
142145
{
143146
private readonly DataGridView owner;
144147

@@ -160,6 +163,16 @@ public RowPresenter(DataGridView owner, GridModel model, LayoutStats stats)
160163

161164
public ItemsRepeater Cells { get; }
162165

166+
public UIElement? CurrentAnchor => null;
167+
168+
public void RegisterAnchorCandidate(UIElement element)
169+
{
170+
}
171+
172+
public void UnregisterAnchorCandidate(UIElement element)
173+
{
174+
}
175+
163176
public void SetRow(int row)
164177
{
165178
Row = row;

‎spikes/wingates2/tool/Program.cs‎

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,14 @@ static void Grid(UIA3Automation automation, Window window, JsonObject o)
6565
o["patterns"] = $"Grid={grid.Patterns.Grid.IsSupported} Table={grid.Patterns.Table.IsSupported} Selection={grid.Patterns.Selection.IsSupported} Scroll={grid.Patterns.Scroll.IsSupported}";
6666
var gp = grid.Patterns.Grid.Pattern;
6767
o["grid_pattern"] = $"RowCount={gp.RowCount.Value} ColumnCount={gp.ColumnCount.Value}";
68+
var tp = grid.Patterns.Table.Pattern;
69+
AutomationElement[] headers = tp.ColumnHeaders.ValueOrDefault ?? [];
70+
o["table_pattern"] = $"column_headers={headers.Length} first='{(headers.Length > 0 ? Name(headers[0]) : "")}' types='{(headers.Length > 0 ? headers[0].ControlType.ToString() : "")}' major={tp.RowOrColumnMajor.ValueOrDefault}";
71+
AutomationElement[] headerItems = grid.FindAllDescendants(cf => cf.ByControlType(ControlType.HeaderItem));
72+
o["header_items_in_tree"] = $"{headerItems.Length} first='{(headerItems.Length > 0 ? Name(headerItems[0]) : "")}'";
73+
var sp = grid.Patterns.Selection.Pattern;
74+
AutomationElement[] selected = sp.Selection.ValueOrDefault ?? [];
75+
o["selection_pattern"] = $"can_select_multiple={sp.CanSelectMultiple.ValueOrDefault} selected={selected.Length} first={(selected.Length > 0 ? Cell(selected[0]) : "")}";
6876
var items = new JsonArray();
6977
foreach (var (r, c) in new[] { (2, 3), (0, 0), (500_000, 150), (999_999, 199) })
7078
{
@@ -80,12 +88,6 @@ static void Grid(UIA3Automation automation, Window window, JsonObject o)
8088
}
8189
}
8290
o["get_item"] = items;
83-
var tp = grid.Patterns.Table.Pattern;
84-
AutomationElement[] headers = tp.ColumnHeaders.ValueOrDefault ?? [];
85-
o["table_pattern"] = $"column_headers={headers.Length} first='{(headers.Length > 0 ? Name(headers[0]) : "")}' types='{(headers.Length > 0 ? headers[0].ControlType.ToString() : "")}' major={tp.RowOrColumnMajor.ValueOrDefault}";
86-
var sp = grid.Patterns.Selection.Pattern;
87-
AutomationElement[] selected = sp.Selection.ValueOrDefault ?? [];
88-
o["selection_pattern"] = $"can_select_multiple={sp.CanSelectMultiple.ValueOrDefault} selected={selected.Length} first={(selected.Length > 0 ? Cell(selected[0]) : "")}";
8991
sw.Restart();
9092
AutomationElement[] cells = grid.FindAllDescendants(cf => cf.ByControlType(ControlType.DataItem));
9193
o["cells_in_uia_tree"] = cells.Length;

0 commit comments

Comments
 (0)