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
355 changes: 354 additions & 1 deletion Tools/windows/Tests/AccessibilityProvider.Native.Tests.cpp

Large diffs are not rendered by default.

28 changes: 28 additions & 0 deletions graphcode-windows/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,34 @@ logical pass, before the physical frame is copied to the window. Header UIA
controls use that same logical layout with one physical-boundary conversion;
the workspace identity region remains separate from the terminal's physical tabs.

### Sidebar scrolling and short windows

The sidebar rows, Needs you, and Activity scroll under the fixed GRAPH/Projects
header and above the footer (status line, update banner, inline error footer).
What is painted, what UIA publishes, and what a click hits are the same rect,
clipped to that scroll region; the right-hand Activity control sits inside the
220-pixel rail.

Keyboard and assistive clients reach offscreen rows through UIA, not a hidden
shortcut: each sidebar list (Projects, Loops, Worktrees) exposes a vertical-only
`IScrollProvider` over the one sidebar offset, and every element that scrolls
with it (rows, controls, the Graph and Quick Chats destinations) exposes
`IScrollItemProvider`; the shell reveals the element through
`Accessibility.wm_sidebar_scroll`. The fixed header, update banner, and error
footer do not scroll and expose no scroll item. The root's coordinate hit-test
returns the element under the point (banner above header above scrolled rows).

Short windows: the sidebar keeps a 52 px scroll region (`Sidebar.min_content_region`,
room for two 24 px row slots; the tests assert the region height and at least one
published element, not two elements at every offset, and a 34 px Needs-you or
Activity card can fill it). If the footer would leave less, the error footer collapses
first, then the update banner; the status line is the floor. A collapsed part is
not painted, published, or clickable. The window's minimum height is the smallest
client height at which the status line, Activity strip, and open Workspace panel
still leave that region (`min_client_height`, enforced through
`WM_GETMINMAXINFO`; width keeps the system minimum). This is layout and message
evidence from App and native provider tests, not a screen-reader or Dev Box run.

## Workspace lifecycle

The Workspace menu lists `Default` and `.graphcode-*` directories under
Expand Down
25 changes: 24 additions & 1 deletion graphcode-windows/src/Accessibility.zig
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ extern fn gc_uia_release(provider: *NativeProvider) void;
extern fn gc_uia_get_object(hwnd: c.HWND, wparam: c.WPARAM, lparam: c.LPARAM, provider: *NativeProvider) c.LRESULT;
extern fn gc_uia_set_status(provider: *NativeProvider, status: [*:0]const u8) c.HRESULT;
extern fn gc_uia_set_canvas_bounds(provider: *NativeProvider, left: c_int, top: c_int, right: c_int, bottom: c_int) c.HRESULT;
extern fn gc_uia_set_sidebar_scroll(provider: *NativeProvider, offset: c_int, max_offset: c_int, viewport: c_int) c.HRESULT;
extern fn gc_uia_set_header_focus(provider: *NativeProvider, identity: ?[*:0]const u8) c.HRESULT;
extern fn gc_uia_update(
provider: *NativeProvider,
Expand Down Expand Up @@ -75,6 +76,8 @@ pub const DynamicElement = struct {
selected: bool = false,
eligible: bool = false,
invokable: bool = true,
/// The element is part of the scrolling sidebar content, so UIA can scroll it into view.
sidebar_scroll: bool = false,
left: i32,
top: i32,
right: i32,
Expand All @@ -98,6 +101,19 @@ pub const uia_workspace_delete_command: usize = 29;
pub const uia_dynamic_invoke_tag: usize = 0x8000000000000000;
pub const uia_dynamic_invoke_mask: usize = 0xC000000000000000;
pub const wm_header_focus: u32 = 0x8000 + 46;
/// Sent by the UIA scroll patterns. wParam is a `SidebarScrollOperation`; lParam is its argument.
/// The window answers 1 when it performed the request and 0 when it could not.
pub const wm_sidebar_scroll: u32 = 0x8000 + 47;
pub const SidebarScrollOperation = struct {
pub const large_decrement: usize = 0;
pub const small_decrement: usize = 1;
pub const large_increment: usize = 3;
pub const small_increment: usize = 4;
/// lParam: the vertical scroll percent in hundredths of a percent.
pub const set_percent: usize = 5;
/// lParam: the identity payload (`worktreeIdentityPayload`) of the element to reveal.
pub const into_view: usize = 6;
};

pub fn worktreeIdentityPayload(path: []const u8) usize {
var hash: u64 = 1469598103934665603;
Expand Down Expand Up @@ -232,7 +248,7 @@ pub const Provider = struct {
parents[index] = element.parent;
selected[index] = if (element.selected) 1 else 0;
eligible[index] = if (element.eligible) 1 else 0;
invokable[index] = if (element.invokable) 1 else 0;
invokable[index] = (if (element.invokable) @as(c_int, 1) else 0) | (if (element.sidebar_scroll) @as(c_int, 2) else 0);
bounds[index * 4] = element.left;
bounds[index * 4 + 1] = element.top;
bounds[index * 4 + 2] = element.right;
Expand Down Expand Up @@ -287,6 +303,13 @@ pub const Provider = struct {
const native = self.native_provider orelse return;
_ = gc_uia_set_canvas_bounds(native, bounds.left, bounds.top, bounds.right, bounds.bottom);
}
/// Reports the sidebar's scroll offset, its maximum, and the visible height so the sidebar
/// lists can answer UIA scroll-pattern properties and raise changes.
pub fn syncSidebarScroll(self: *Provider, offset: i32, max_offset: i32, viewport: i32) void {
if (!builtin.link_libc) return;
const native = self.native_provider orelse return;
_ = gc_uia_set_sidebar_scroll(native, offset, max_offset, viewport);
}
pub fn add(self: *Provider, element: Element) !usize {
const index = self.elements.items.len;
try self.elements.append(element);
Expand Down
Loading
Loading