Repository navigation
Fix Windows sidebar scrolled-control painting, UIA hit-testing, UIA scrolling and short windows - #685
Merged
Merged
Conversation
…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
force-pushed
the
coneilen-fix-sidebar-scroll-uia
branch
from
October 10, 2026 07:06
10fb891 to
9915c54
Compare
This was referenced Oct 10, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
Sidebar.zigonly.Sidebar.drawcalledneedsYouStopBounds,activityFilterBounds,activityControlBoundsandactivityCardBoundswith offset 0 while the row text beside them used the real offset. It now passesscroll_offset; the existing draw clip keeps them inside the scroll region. The finding also namedApp.zigaround 9095-9149 and 7214-7245: those are the click and UIA paths and already passapp.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.ElementProviderFromPointalways 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.applyUiaDynamicInvokedirectly 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.IScrollProviderover the single sidebar offset (state published through a newgc_uia_set_sidebar_scroll), and every element that scrolls with the sidebar (rows, controls, Needs-you, Activity, the two destinations) exposesIScrollItemProvider; the fixed header, banner and error footer do not. The provider sendswm_sidebar_scrolland the shell answers whether it moved the viewport; a refusal is surfaced asUIA_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_GETMINMAXINFOsets 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), andinvestigation/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:
scrolled Needs you and Activity controls are painted where UIA publishes them and clicks landSidebar.drawhas 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 sendsstopNodenativeHitTestChecksnativeScrollChecksIScrollProvideron 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,IScrollItemProvideron scrolling rows and the Graph destination and not on header or banner, shell refusal becomes an errorUIA scroll-into-view brings every sidebar element fully into the scroll region ...UIA scroll requests move the one sidebar offset and refuse what they cannot doevery scrolling sidebar element advertises scroll-into-view ...sidebar footer collapses the error footer then the banner ...and Sidebar:footer parts collapse ...the minimum window height keeps a 52 px sidebar scroll region and at least one published row ...WM_GETMINMAXINFOraises (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 regionnativeBannerInvokeChecksand App:banner click and the UIA dynamic invoke command open the same update offerIInvokeProvider::Invokeon the banner posts (not runs) exactly 0x83fd6a5f429785e1. App: that same command and a click at the banner centre each reachshowCurrentUpdateOfferand yield the same version and release URL, through a new test-only observer; with no update neither does; a collapsed banner is not clickableCommands (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:
ElementFromPointagainst the running shell. The DIB capture is GDI drawn into a memory bitmap at logical coordinates.Tools\windows\uia-live-gate.ps1andvalidate.ps1 -Task windows-shellwere 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.WM_GETMINMAXINFOand 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 testandmake checkare 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.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
git commit -s) per the DCOmake test) - macOS target, not run; Windows roots listed above passmake check) - macOS target, not run