Skip to content

Fix Windows sidebar scrolled-control painting, UIA hit-testing, UIA scrolling and short windows - #685

Merged
coneilen merged 3 commits into
mainfrom
coneilen-fix-sidebar-scroll-uia
Oct 10, 2026
Merged

coneilen merged 3 commits into
mainfrom
coneilen-fix-sidebar-scroll-uia

Conversation

@coneilen

@coneilen coneilen commented Oct 10, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Fixes the sidebar scroll and UIA defects left by #675 and #671 in the Windows shell: scrolled Needs-you and Activity controls were painted away from where UIA and clicks put them, UIA coordinate hit-testing always returned the focused element, assistive clients had no way to scroll an offscreen row into view, and a short window could leave no sidebar rows at all.

This is Windows-only. Nothing here was run on a Dev Box, with a screen reader, or against the live UIA gate; see "Not tested".

Changes

Review findings, each checked against the code first:

  1. Scrolled Needs-you / Activity painted at the unscrolled position - confirmed in Sidebar.zig only. Sidebar.draw called needsYouStopBounds, activityFilterBounds, activityControlBounds and activityCardBounds with offset 0 while the row text beside them used the real offset. It now passes scroll_offset; the existing draw clip keeps them inside the scroll region. The finding also named App.zig around 9095-9149 and 7214-7245: those are the click and UIA paths and already pass app.sidebar_scroll / self.sidebar_scroll, so no change there. While testing this I found a second, related mismatch: the Activity right arrow was published and clickable at x 208..230 but the rail is 220 wide, so it was painted half-clipped. Both arrows now sit inside the rail (172..194 and 196..218), which is a small visible layout change.
  2. ElementProviderFromPoint always returned the focused element - confirmed (AccessibilityProvider.cpp). It now resolves the screen point against the published rows, the Graph / Quick Chats destinations, the banner, the header and the canvas. Paint order: error footer and update banner above header above scrolled rows; within a layer the smallest rect wins (a Stop button over its row), a later row wins a tie; empty (scrolled-out) rects are never hit; nothing under the point returns no element.
  3. Banner Invoke test did not prove COM Invoke - confirmed (the test called applyUiaDynamicInvoke directly with no update available). The behaviour itself was already correct, so this is added evidence, not a fix. See the two linked tests below; they are not one test spanning COM to the update dialog.
  4. No scrolling, and a ~300 px client could show no rows - confirmed. Decision, in the smallest form that is correct for assistive clients and keeps layout honest: (a) UIA scrolling: the Projects, Loops and Worktrees lists expose a vertical-only IScrollProvider over the single sidebar offset (state published through a new gc_uia_set_sidebar_scroll), and every element that scrolls with the sidebar (rows, controls, Needs-you, Activity, the two destinations) exposes IScrollItemProvider; the fixed header, banner and error footer do not. The provider sends wm_sidebar_scroll and the shell answers whether it moved the viewport; a refusal is surfaced as UIA_E_INVALIDOPERATION. (b) A short window keeps a 52 px scroll region (Sidebar.min_content_region, room for two 24 px rows, not a guarantee of two elements at every offset): the inline error footer collapses first, then the update banner, the status line is the floor, and a collapsed part is not painted, published or clickable. (c) WM_GETMINMAXINFO sets a minimum height (517 logical px = header, two rows, status line, Workspace panel, Activity strip); width keeps the system minimum. Behaviour change to review: with the Workspace panel open and an update available, windows shorter than about 575 logical px now hide the banner (it was shown with a 24 px content region before). The update offer is still reachable from the Check for Updates menu command.

Files: Sidebar.zig, App.zig, Accessibility.zig, AccessibilityProvider.cpp, AccessibilityProvider.Native.Tests.cpp, graphcode-windows/README.md (the decision), and investigation/ui-parity-matrix.md (the Sidebar update banner row gains the collapse and the evidence limits; the UI Automation tree row stays Partial). The provider's flag word for published rows gained bit 1 (scrolls with the sidebar); bit 0 is still invokable.

Test plan

What each new test proves, and whether it was RED first:

Test Proves RED before the fix?
App: scrolled Needs you and Activity controls are painted where UIA publishes them and clicks land At 96 and 144 DPI, heights 820/600/500/400/300, banner shown or not, 40 loops, offsets chosen to leave each control visible, clipped by the header, clipped by the footer, and scrolled away: a DIB capture of Sidebar.draw has none of the Stop / Activity fill colours outside the clipped control rects, the first and last visible row of each control is painted, the UIA rect equals the clipped drawn rect, the centre hits the right control, and a real click on a Stop sends stopNode Yes, behavioural: Stop painted at the unscrolled position. After that fix it failed a second time on the right arrow (published past the rail), which is the 172/196 change
Native: nativeHitTestChecks Visible rows, destinations, nested control over row, header over a row published under it, banner over a row, empty region, scrolled-out row, outside, edge inclusivity, canvas only when reported Yes, behavioural: 16 failures (every point returned the root)
Native: nativeScrollChecks IScrollProvider on the three lists and not on the root, scrollable / percent / view size (25, 33.3) / clamp, horizontal refused, out-of-range percent refused, shell messages carry the right operation and argument, IScrollItemProvider on scrolling rows and the Graph destination and not on header or banner, shell refusal becomes an error Yes, behavioural for pattern presence (7 failures); the percent and view-size checks need the new export, so those are compile-level RED only
App: UIA scroll-into-view brings every sidebar element fully into the scroll region ... From scroll 0 and max, 96 and 144 DPI, heights 820/500/400/300, every scrolling element ends fully inside the region (or region-sized if taller); 1000+ reveals New API, compile-level RED only
App: UIA scroll requests move the one sidebar offset and refuse what they cannot do page, line, percent, clamp; refuses bad percent, bad operation, banner, header, unknown identity New API, compile-level RED only
App: every scrolling sidebar element advertises scroll-into-view ... Published flag is true for every sidebar element and false for banner and error footer; published scroll state equals the real offset, maximum and viewport New API, compile-level RED only
App: sidebar footer collapses the error footer then the banner ... and Sidebar: footer parts collapse ... A 52 px region is kept whenever the status line alone leaves room; collapse order and exact collapse points; a collapsed banner / error footer is not published, painted (pixel probe) or clickable The two-row requirement was RED (5 px of content at 520 px with panel and error footer); collapse behaviour itself is new API
App: the minimum window height keeps a 52 px sidebar scroll region and at least one published row ... WM_GETMINMAXINFO raises (never lowers) the minimum to 517 logical px at 96 and 144 DPI; at that height every panel / banner / error combination leaves a 52 px region (room for two 24 px row slots) and at scroll 0 at least one published scrolling element. It does NOT assert two elements at every offset: a 34 px Needs-you or Activity card can fill the region Compile-level RED only (new message handler)
Native: nativeBannerInvokeChecks and App: banner click and the UIA dynamic invoke command open the same update offer Native: COM IInvokeProvider::Invoke on the banner posts (not runs) exactly 0x83fd6a5f429785e1. App: that same command and a click at the banner centre each reach showCurrentUpdateOffer and yield the same version and release URL, through a new test-only observer; with no update neither does; a collapsed banner is not clickable No RED. COM Invoke already posted the right command; this only adds evidence. The two tests share the literal; they are not one test spanning COM to the dialog

Commands (run with USERPROFILE, LOCALAPPDATA, APPDATA, TEMP, TMP and GRAPHCODE_SUPPORT_DIR pointed at a scratch profile, tools from Tools\windows\bootstrap.ps1: Zig 0.15.2, Winghostty at the pinned commit):

RED: zig test src\App.zig ... --test-filter "scrolled Needs you and Activity" -> stop 0 not painted over its clipped rect 154,576,206,596 at 96 DPI, height 820, scroll 1238; FAIL SidebarControlNotPaintedWhereItIsPublished
GREEN: zig test src\App.zig ... --test-filter "scrolled Needs you and Activity" -> 1/1 passed
REGRESSION: zig test src\App.zig src\AccessibilityProvider.cpp ... -> 1009/1009 passed; Sidebar.zig 199/199, GraphCanvas.zig 213/213, Accessibility.zig 4/4, AccessibilityProvider.Native.Tests.ps1 -> 170 checks, 0 failures

Other RED runs, kept as logs and not folded into the line above: native hit-test run -> 111 checks, 16 failures; native scroll run -> 149 checks, 7 failures; two-row minimum -> only 5px of sidebar content at client height 520, panel true, update false, error true; with the arrow positions reverted the paint test fails with right 0 not painted over its clipped rect 208,656,230,678. The first REGRESSION run before rebasing was 1004/1004 App tests; after rebasing onto origin/main (which added the AltGr terminal change) it is 1009/1009.

Not tested, and not claimed:

  • No Dev Box, no real pixels on screen, no screen reader or Narrator, no live UIA client calling the new patterns or ElementFromPoint against the running shell. The DIB capture is GDI drawn into a memory bitmap at logical coordinates.
  • Tools\windows\uia-live-gate.ps1 and validate.ps1 -Task windows-shell were NOT run here (they launch the shell and a fixture daemon, which this machine's isolation rules forbid for this session). From reading the gate: its ingress error evidence is the canvas inline error and does not use the sidebar footer; the sidebar error footer, Needs-you Stop (via InvokePattern) and activity scroll (via UIA) expectations need a window tall enough that the footer is not collapsed, which holds for the default 820 px window but was not run. CI is the first run of those.
  • The collapsed-footer and minimum-height behaviour is unit and message evidence only; the minimum height is applied through WM_GETMINMAXINFO and was only exercised by calling the window procedure with a frameless test window, not by resizing the real frame with its menu bar and title.
  • make test and make check are macOS targets and were not run; nothing under the macOS paths changed. The IScrollProvider / ElementProviderFromPoint contracts follow the UIA documentation but were not conformance-tested with a UIA client.
  • The parity ledger rows stay as they were except for the added text; no row changed status.

Review follow-up: the native hit-test now also runs from a window at a non-zero screen origin with physical-pixel (1.5x) bounds. The provider does no DPI conversion of its own, so this exercises origin handling only, not a DPI-aware process. Re-run after rebasing on #680/#681: App 1019/1019, Sidebar 199/199, native 176 checks, 0 failures.

Checklist

  • I have read the Contributing Guidelines
  • I have signed off my commits (git commit -s) per the DCO
  • Tests pass locally (make test) - macOS target, not run; Windows roots listed above pass
  • Code follows the existing style (make check) - macOS target, not run
  • I added the test/contract before the implementation and observed the intended RED failure - for the paint, hit-test, scroll-pattern and two-row tests; not for the banner Invoke evidence, which was already correct

coneilen and others added 3 commits October 10, 2026 00:00
…nd short windows

Scrolled Needs-you Stop buttons and the Activity filter, arrows and cards
were painted with a zero scroll offset while click handling and UIA used the
real one, so after scrolling they were clipped or drawn away from where UIA
and the mouse believed they were. Sidebar.draw now passes the real offset.
The right-hand Activity control also extended 10 px past the 220 px rail, so
it was painted half-clipped but published and clickable in full; both arrows
now sit inside the rail.

The native UIA provider's ElementProviderFromPoint returned the focused
element for every point. It now resolves the point against the published
rows, destinations, banner, header, and canvas with paint-order tiers and
returns nothing where nothing is drawn.

The provider had no IScrollProvider or IScrollItemProvider, so keyboard and
assistive clients could not bring an offscreen row into view. The sidebar
lists now expose a vertical-only IScrollProvider over the one sidebar offset
and every scrolling sidebar element exposes IScrollItemProvider; the shell
answers a new wm_sidebar_scroll message by moving that offset.

On short windows the footer could leave no sidebar rows at all. The sidebar
now keeps two rows of scroll region by collapsing the error footer, then the
update banner, and the window's minimum height is the smallest at which the
status line, Activity strip and Workspace panel still leave that region.

Tests: native provider tests for point resolution (rows, destinations,
nested controls, header and banner z-order, scrolled-out rows, canvas), the
scroll patterns and their shell messages, and the banner's COM Invoke
command; App tests that diff painted pixels against published UIA rects and
click targets for scrolled Needs-you and Activity at 96 and 144 DPI and
heights 820 to 300, footer collapse, minimum height, scroll-into-view of
every sidebar element, and banner click versus the invoke command opening
the same offer. No Dev Box pixels or screen reader were involved.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Colin Neilens <coneilen@microsoft.com>
…ed origin

The minimum is a 52 px scroll region, not two published elements at every
offset: rename the tests and docs to say so. Add a physical-pixel,
non-zero-origin ElementProviderFromPoint check to the native provider test.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Colin Neilens <coneilen@microsoft.com>
…ive edge

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Colin Neilens <coneilen@microsoft.com>
@coneilen
coneilen force-pushed the coneilen-fix-sidebar-scroll-uia branch from 10fb891 to 9915c54 Compare October 10, 2026 07:06
@coneilen
coneilen merged commit d42200a into main Oct 10, 2026
25 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant