From 3f4a838c1b5a91494e23fa903482a77621da6056 Mon Sep 17 00:00:00 2001 From: Colin Neilens Date: Thu, 1 Oct 2026 18:54:51 -0700 Subject: [PATCH 1/2] Expose Windows onboarding to UI Automation Part of #556. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Signed-off-by: Colin Neilens --- .../src/AccessibilityProvider.cpp | 2 + .../src/OnboardingAccessibilityProvider.cpp | 578 ++++++++++++++++++ graphcode-windows/src/WindowsOnboarding.zig | 265 ++++++-- 3 files changed, 811 insertions(+), 34 deletions(-) create mode 100644 graphcode-windows/src/OnboardingAccessibilityProvider.cpp diff --git a/graphcode-windows/src/AccessibilityProvider.cpp b/graphcode-windows/src/AccessibilityProvider.cpp index d442f5a0..ef6496f8 100644 --- a/graphcode-windows/src/AccessibilityProvider.cpp +++ b/graphcode-windows/src/AccessibilityProvider.cpp @@ -1169,3 +1169,5 @@ extern "C" HRESULT gc_uia_set_canvas_bounds(IRawElementProviderSimple *provider, static_cast(provider)->setCanvasBounds(left, top, right, bottom); return S_OK; } + +#include "OnboardingAccessibilityProvider.cpp" diff --git a/graphcode-windows/src/OnboardingAccessibilityProvider.cpp b/graphcode-windows/src/OnboardingAccessibilityProvider.cpp new file mode 100644 index 00000000..2ea44996 --- /dev/null +++ b/graphcode-windows/src/OnboardingAccessibilityProvider.cpp @@ -0,0 +1,578 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +namespace onboarding_uia { + +constexpr UINT kFocusMessage = WM_APP + 60; + +enum NodeId : int { + kRoot = 0, + kPage = 1, + kSkip = 2, + kBack = 3, + kPrimary = 4, + kClaudeCode = 5, + kCopilotCli = 6, + kCodex = 7, +}; + +struct State; + +class Node final : public IRawElementProviderSimple, + public IRawElementProviderFragment, + public IRawElementProviderFragmentRoot, + public IInvokeProvider { + public: + Node(std::shared_ptr state, int id); + + HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid, void **out) override; + ULONG STDMETHODCALLTYPE AddRef() override; + ULONG STDMETHODCALLTYPE Release() override; + HRESULT STDMETHODCALLTYPE get_ProviderOptions(ProviderOptions *value) override; + HRESULT STDMETHODCALLTYPE GetPatternProvider(PATTERNID id, IUnknown **value) override; + HRESULT STDMETHODCALLTYPE GetPropertyValue(PROPERTYID property, VARIANT *value) override; + HRESULT STDMETHODCALLTYPE get_HostRawElementProvider( + IRawElementProviderSimple **value) override; + HRESULT STDMETHODCALLTYPE Navigate( + NavigateDirection direction, + IRawElementProviderFragment **value) override; + HRESULT STDMETHODCALLTYPE GetRuntimeId(SAFEARRAY **value) override; + HRESULT STDMETHODCALLTYPE get_BoundingRectangle(UiaRect *value) override; + HRESULT STDMETHODCALLTYPE GetEmbeddedFragmentRoots(SAFEARRAY **value) override; + HRESULT STDMETHODCALLTYPE SetFocus() override; + HRESULT STDMETHODCALLTYPE get_FragmentRoot( + IRawElementProviderFragmentRoot **value) override; + HRESULT STDMETHODCALLTYPE ElementProviderFromPoint( + double x, double y, IRawElementProviderFragment **value) override; + HRESULT STDMETHODCALLTYPE GetFocus( + IRawElementProviderFragment **value) override; + HRESULT STDMETHODCALLTYPE Invoke() override; + + void shutdown(); + HRESULT update(int page, int focused, int backend); + + private: + bool availableLocked() const; + bool invokable() const; + std::vector childrenLocked() const; + Node *createRetainedLocked(int id) const; + HRESULT focusedElement(IRawElementProviderFragment **value); + + std::shared_ptr state_; + int id_; + volatile LONG refs_ = 1; +}; + +struct State { + std::mutex mutex; + HWND hwnd{}; + Node *root{}; + int page = 0; + int focused = kPrimary; + int backend = 0; + bool active = true; +}; + +static bool isAction(int id) { + return id >= kSkip && id <= kCodex; +} + +static bool hasNativeFocus(HWND hwnd) { + GUITHREADINFO info{}; + info.cbSize = sizeof(info); + return GetGUIThreadInfo(GetWindowThreadProcessId(hwnd, nullptr), &info) && + info.hwndFocus == hwnd && GetForegroundWindow() == hwnd && + IsWindowEnabled(hwnd); +} + +static bool isAvailable(int id, int page) { + if (id == kRoot || id == kPage || id == kSkip || id == kPrimary) return true; + if (id == kBack) return page > 0; + if (id >= kClaudeCode && id <= kCodex) return page == 3; + return false; +} + +static std::vector childrenForPage(int page) { + std::vector children{kPage, kSkip}; + if (page > 0) children.push_back(kBack); + if (page == 3) { + children.push_back(kClaudeCode); + children.push_back(kCopilotCli); + children.push_back(kCodex); + } + children.push_back(kPrimary); + return children; +} + +static const wchar_t *pageName(int page) { + static const wchar_t *names[] = { + L"Welcome to GraphCode: Agents you can watch", + L"How to read a loop", + L"Four kinds of loop", + L"Which agent runs them", + }; + return names[page >= 0 && page < 4 ? page : 0]; +} + +static const wchar_t *automationId(int id) { + static const wchar_t *ids[] = { + L"onboarding-root", + L"onboarding-page", + L"onboarding-skip", + L"onboarding-back", + L"onboarding-primary", + L"onboarding-backend-claude-code", + L"onboarding-backend-copilot-cli", + L"onboarding-backend-codex", + }; + return ids[id >= kRoot && id <= kCodex ? id : kRoot]; +} + +static const wchar_t *nameFor(int id, int page) { + switch (id) { + case kRoot: return L"GraphCode onboarding"; + case kPage: return pageName(page); + case kSkip: return L"Skip onboarding"; + case kBack: return L"Back"; + case kPrimary: return page == 3 ? L"Get Started" : L"Continue"; + case kClaudeCode: return L"Claude Code"; + case kCopilotCli: return L"Copilot CLI"; + case kCodex: return L"Codex"; + default: return L"GraphCode onboarding"; + } +} + +static CONTROLTYPEID controlType(int id) { + if (id == kRoot) return UIA_WindowControlTypeId; + if (id == kPage) return UIA_PaneControlTypeId; + if (id >= kClaudeCode && id <= kCodex) return UIA_RadioButtonControlTypeId; + return UIA_ButtonControlTypeId; +} + +static RECT clientBounds(int id, int page) { + switch (id) { + case kPage: return RECT{32, 56, 528, 536}; + case kSkip: return RECT{474, 12, 540, 42}; + case kBack: return RECT{20, 564, 102, 604}; + case kPrimary: return RECT{418, 564, 540, 604}; + case kClaudeCode: return RECT{58, 182, 502, 248}; + case kCopilotCli: return RECT{58, 258, 502, 324}; + case kCodex: return RECT{58, 334, 502, 400}; + default: + if (page >= 0) return RECT{0, 0, 560, 620}; + return RECT{}; + } +} + +static HRESULT focusWindow(HWND hwnd) { + const DWORD current_thread = GetCurrentThreadId(); + const DWORD window_thread = GetWindowThreadProcessId(hwnd, nullptr); + const bool attach = window_thread != 0 && window_thread != current_thread; + if (attach && !AttachThreadInput(current_thread, window_thread, TRUE)) + return HRESULT_FROM_WIN32(GetLastError()); + SetForegroundWindow(hwnd); + SetLastError(ERROR_SUCCESS); + if (::SetFocus(hwnd) == nullptr) { + const DWORD error = GetLastError(); + if (attach) AttachThreadInput(current_thread, window_thread, FALSE); + if (error != ERROR_SUCCESS) return HRESULT_FROM_WIN32(error); + } + if (attach) AttachThreadInput(current_thread, window_thread, FALSE); + return S_OK; +} + +Node::Node(std::shared_ptr state, int id) + : state_(std::move(state)), id_(id) { + if (id_ == kRoot) { + std::lock_guard lock(state_->mutex); + state_->root = this; + } +} + +HRESULT STDMETHODCALLTYPE Node::QueryInterface(REFIID iid, void **out) { + if (!out) return E_POINTER; + *out = nullptr; + if (iid == IID_IUnknown || iid == __uuidof(IRawElementProviderSimple)) + *out = static_cast(this); + else if (iid == __uuidof(IRawElementProviderFragment)) + *out = static_cast(this); + else if (iid == __uuidof(IRawElementProviderFragmentRoot) && id_ == kRoot) + *out = static_cast(this); + else if (iid == __uuidof(IInvokeProvider) && invokable()) + *out = static_cast(this); + else + return E_NOINTERFACE; + AddRef(); + return S_OK; +} + +ULONG STDMETHODCALLTYPE Node::AddRef() { + return static_cast(InterlockedIncrement(&refs_)); +} + +ULONG STDMETHODCALLTYPE Node::Release() { + const ULONG value = static_cast(InterlockedDecrement(&refs_)); + if (value == 0) delete this; + return value; +} + +HRESULT STDMETHODCALLTYPE Node::get_ProviderOptions(ProviderOptions *value) { + if (!value) return E_POINTER; + *value = static_cast( + ProviderOptions_ServerSideProvider | + ProviderOptions_UseComThreading | + ProviderOptions_ProviderOwnsSetFocus); + return S_OK; +} + +HRESULT STDMETHODCALLTYPE Node::GetPatternProvider(PATTERNID id, IUnknown **value) { + if (!value) return E_POINTER; + *value = nullptr; + if (id != UIA_InvokePatternId || !invokable()) return S_FALSE; + AddRef(); + *value = static_cast(this); + return S_OK; +} + +HRESULT STDMETHODCALLTYPE Node::GetPropertyValue(PROPERTYID property, VARIANT *value) { + if (!value) return E_POINTER; + VariantInit(value); + int page = 0; + int focused = 0; + int backend = 0; + { + std::lock_guard lock(state_->mutex); + if (!availableLocked()) return UIA_E_ELEMENTNOTAVAILABLE; + page = state_->page; + focused = state_->focused; + backend = state_->backend; + } + if (property == UIA_NamePropertyId || + property == UIA_AutomationIdPropertyId || + property == UIA_ItemStatusPropertyId) { + const wchar_t *text = L""; + if (property == UIA_NamePropertyId) text = nameFor(id_, page); + if (property == UIA_AutomationIdPropertyId) text = automationId(id_); + if (property == UIA_ItemStatusPropertyId && + id_ >= kClaudeCode && id_ <= kCodex) { + text = backend == id_ - kClaudeCode ? L"Selected" : L"Not selected"; + } + value->vt = VT_BSTR; + value->bstrVal = SysAllocString(text); + return value->bstrVal ? S_OK : E_OUTOFMEMORY; + } + if (property == UIA_ControlTypePropertyId) { + value->vt = VT_I4; + value->lVal = controlType(id_); + return S_OK; + } + if (property == UIA_IsEnabledPropertyId || + property == UIA_IsControlElementPropertyId || + property == UIA_IsContentElementPropertyId || + property == UIA_IsKeyboardFocusablePropertyId || + property == UIA_HasKeyboardFocusPropertyId) { + bool result = true; + if (property == UIA_IsKeyboardFocusablePropertyId) result = isAction(id_); + if (property == UIA_HasKeyboardFocusPropertyId) + result = focused == id_ && hasNativeFocus(state_->hwnd); + value->vt = VT_BOOL; + value->boolVal = result ? VARIANT_TRUE : VARIANT_FALSE; + return S_OK; + } + return S_FALSE; +} + +HRESULT STDMETHODCALLTYPE Node::get_HostRawElementProvider( + IRawElementProviderSimple **value) { + if (!value) return E_POINTER; + *value = nullptr; + HWND hwnd = nullptr; + { + std::lock_guard lock(state_->mutex); + if (!availableLocked()) return UIA_E_ELEMENTNOTAVAILABLE; + if (id_ != kRoot) return S_OK; + hwnd = state_->hwnd; + } + return UiaHostProviderFromHwnd(hwnd, value); +} + +HRESULT STDMETHODCALLTYPE Node::Navigate( + NavigateDirection direction, IRawElementProviderFragment **value) { + if (!value) return E_POINTER; + *value = nullptr; + Node *target = nullptr; + { + std::lock_guard lock(state_->mutex); + if (!availableLocked()) return UIA_E_ELEMENTNOTAVAILABLE; + if (direction == NavigateDirection_Parent && id_ != kRoot) { + target = createRetainedLocked(kRoot); + } else if (id_ == kRoot && + (direction == NavigateDirection_FirstChild || + direction == NavigateDirection_LastChild)) { + const auto children = childrenLocked(); + if (!children.empty()) { + target = createRetainedLocked( + direction == NavigateDirection_FirstChild + ? children.front() + : children.back()); + } + } else if (id_ != kRoot && + (direction == NavigateDirection_NextSibling || + direction == NavigateDirection_PreviousSibling)) { + const auto siblings = childrenLocked(); + const auto current = std::find(siblings.begin(), siblings.end(), id_); + if (current != siblings.end()) { + if (direction == NavigateDirection_NextSibling && + current + 1 != siblings.end()) + target = createRetainedLocked(*(current + 1)); + if (direction == NavigateDirection_PreviousSibling && + current != siblings.begin()) + target = createRetainedLocked(*(current - 1)); + } + } + } + if (target) *value = static_cast(target); + return S_OK; +} + +HRESULT STDMETHODCALLTYPE Node::GetRuntimeId(SAFEARRAY **value) { + if (!value) return E_POINTER; + *value = nullptr; + { + std::lock_guard lock(state_->mutex); + if (!availableLocked()) return UIA_E_ELEMENTNOTAVAILABLE; + } + if (id_ == kRoot) return S_OK; + *value = SafeArrayCreateVector(VT_I4, 0, 3); + if (!*value) return E_OUTOFMEMORY; + LONG values[] = {UiaAppendRuntimeId, 0x47434f, id_}; + for (LONG index = 0; index < 3; ++index) + SafeArrayPutElement(*value, &index, &values[index]); + return S_OK; +} + +HRESULT STDMETHODCALLTYPE Node::get_BoundingRectangle(UiaRect *value) { + if (!value) return E_POINTER; + HWND hwnd = nullptr; + int page = 0; + { + std::lock_guard lock(state_->mutex); + if (!availableLocked()) return UIA_E_ELEMENTNOTAVAILABLE; + hwnd = state_->hwnd; + page = state_->page; + } + RECT bounds{}; + if (id_ == kRoot) + GetClientRect(hwnd, &bounds); + else + bounds = clientBounds(id_, page); + POINT origin{0, 0}; + ClientToScreen(hwnd, &origin); + value->left = origin.x + bounds.left; + value->top = origin.y + bounds.top; + value->width = bounds.right - bounds.left; + value->height = bounds.bottom - bounds.top; + return S_OK; +} + +HRESULT STDMETHODCALLTYPE Node::GetEmbeddedFragmentRoots(SAFEARRAY **value) { + if (!value) return E_POINTER; + *value = nullptr; + return S_OK; +} + +HRESULT STDMETHODCALLTYPE Node::SetFocus() { + if (!isAction(id_)) return UIA_E_INVALIDOPERATION; + HWND hwnd = nullptr; + bool changed = false; + { + std::lock_guard lock(state_->mutex); + if (!availableLocked()) return UIA_E_ELEMENTNOTAVAILABLE; + changed = state_->focused != id_; + state_->focused = id_; + hwnd = state_->hwnd; + } + const HRESULT result = focusWindow(hwnd); + if (FAILED(result)) return result; + PostMessageW(hwnd, kFocusMessage, static_cast(id_), 0); + if (changed) + UiaRaiseAutomationEvent( + static_cast(this), + UIA_AutomationFocusChangedEventId); + return S_OK; +} + +HRESULT STDMETHODCALLTYPE Node::get_FragmentRoot( + IRawElementProviderFragmentRoot **value) { + if (!value) return E_POINTER; + *value = nullptr; + std::lock_guard lock(state_->mutex); + if (!availableLocked() || !state_->root) + return UIA_E_ELEMENTNOTAVAILABLE; + state_->root->AddRef(); + *value = static_cast(state_->root); + return S_OK; +} + +HRESULT STDMETHODCALLTYPE Node::ElementProviderFromPoint( + double x, double y, IRawElementProviderFragment **value) { + if (!value) return E_POINTER; + *value = nullptr; + HWND hwnd = nullptr; + int page = 0; + std::vector children; + { + std::lock_guard lock(state_->mutex); + if (!availableLocked()) return UIA_E_ELEMENTNOTAVAILABLE; + hwnd = state_->hwnd; + page = state_->page; + children = childrenLocked(); + } + POINT point{static_cast(x), static_cast(y)}; + ScreenToClient(hwnd, &point); + for (auto current = children.rbegin(); current != children.rend(); ++current) { + const RECT bounds = clientBounds(*current, page); + if (PtInRect(&bounds, point)) { + auto *node = new (std::nothrow) Node(state_, *current); + if (!node) return E_OUTOFMEMORY; + *value = static_cast(node); + return S_OK; + } + } + return S_OK; +} + +HRESULT STDMETHODCALLTYPE Node::GetFocus( + IRawElementProviderFragment **value) { + return focusedElement(value); +} + +HRESULT STDMETHODCALLTYPE Node::Invoke() { + HWND hwnd = nullptr; + { + std::lock_guard lock(state_->mutex); + if (!availableLocked() || !isAction(id_)) + return UIA_E_ELEMENTNOTENABLED; + hwnd = state_->hwnd; + } + return PostMessageW(hwnd, WM_COMMAND, static_cast(id_), 0) + ? S_OK + : HRESULT_FROM_WIN32(GetLastError()); +} + +void Node::shutdown() { + if (id_ != kRoot) return; + std::lock_guard lock(state_->mutex); + state_->active = false; + state_->root = nullptr; +} + +HRESULT Node::update(int page, int focused, int backend) { + bool page_changed = false; + bool focus_changed = false; + { + std::lock_guard lock(state_->mutex); + if (!state_->active) return UIA_E_ELEMENTNOTAVAILABLE; + page_changed = state_->page != page; + focus_changed = state_->focused != focused; + state_->page = page; + state_->focused = focused; + state_->backend = backend; + } + if (page_changed) + UiaRaiseStructureChangedEvent( + static_cast(this), + StructureChangeType_ChildrenInvalidated, nullptr, 0); + if (focus_changed) { + auto *focused_node = new (std::nothrow) Node(state_, focused); + if (!focused_node) return E_OUTOFMEMORY; + UiaRaiseAutomationEvent( + static_cast(focused_node), + UIA_AutomationFocusChangedEventId); + focused_node->Release(); + } + return S_OK; +} + +bool Node::availableLocked() const { + return state_->active && isAvailable(id_, state_->page); +} + +bool Node::invokable() const { + std::lock_guard lock(state_->mutex); + return availableLocked() && isAction(id_); +} + +std::vector Node::childrenLocked() const { + return childrenForPage(state_->page); +} + +Node *Node::createRetainedLocked(int id) const { + if (!isAvailable(id, state_->page)) return nullptr; + if (id == kRoot) { + if (!state_->root) return nullptr; + state_->root->AddRef(); + return state_->root; + } + return new (std::nothrow) Node(state_, id); +} + +HRESULT Node::focusedElement(IRawElementProviderFragment **value) { + if (!value) return E_POINTER; + *value = nullptr; + int focused = 0; + HWND hwnd = nullptr; + { + std::lock_guard lock(state_->mutex); + if (!availableLocked()) return UIA_E_ELEMENTNOTAVAILABLE; + focused = state_->focused; + hwnd = state_->hwnd; + if (!isAvailable(focused, state_->page)) return S_OK; + } + if (!hasNativeFocus(hwnd)) return S_OK; + auto *node = new (std::nothrow) Node(state_, focused); + if (!node) return E_OUTOFMEMORY; + *value = static_cast(node); + return S_OK; +} + +} // namespace onboarding_uia + +extern "C" IRawElementProviderSimple *gc_onboarding_uia_create(HWND hwnd) { + auto state = std::make_shared(); + state->hwnd = hwnd; + return new (std::nothrow) onboarding_uia::Node( + std::move(state), onboarding_uia::kRoot); +} + +extern "C" void gc_onboarding_uia_release( + IRawElementProviderSimple *provider) { + if (!provider) return; + auto *root = static_cast(provider); + root->shutdown(); + root->Release(); +} + +extern "C" LRESULT gc_onboarding_uia_get_object( + HWND hwnd, WPARAM wparam, LPARAM lparam, + IRawElementProviderSimple *provider) { + if (!provider || lparam != UiaRootObjectId) return 0; + return UiaReturnRawElementProvider(hwnd, wparam, lparam, provider); +} + +extern "C" HRESULT gc_onboarding_uia_update( + IRawElementProviderSimple *provider, int page, int focused, int backend) { + if (!provider || page < 0 || page > 3 || + backend < 0 || backend > 2 || + !onboarding_uia::isAvailable(focused, page) || + !onboarding_uia::isAction(focused)) + return E_INVALIDARG; + auto *root = static_cast(provider); + return root->update(page, focused, backend); +} diff --git a/graphcode-windows/src/WindowsOnboarding.zig b/graphcode-windows/src/WindowsOnboarding.zig index 19945459..df7c4e50 100644 --- a/graphcode-windows/src/WindowsOnboarding.zig +++ b/graphcode-windows/src/WindowsOnboarding.zig @@ -4,6 +4,22 @@ const Win32 = @import("Win32.zig"); const c = Win32.c; const ModalTeardown = @import("ModalTeardown.zig"); +const NativeProvider = opaque {}; +extern fn gc_onboarding_uia_create(hwnd: c.HWND) ?*NativeProvider; +extern fn gc_onboarding_uia_release(provider: *NativeProvider) void; +extern fn gc_onboarding_uia_get_object( + hwnd: c.HWND, + wparam: c.WPARAM, + lparam: c.LPARAM, + provider: *NativeProvider, +) c.LRESULT; +extern fn gc_onboarding_uia_update( + provider: *NativeProvider, + page: c_int, + focused: c_int, + backend: c_int, +) c.HRESULT; + pub const page_count: u8 = 4; pub const Backend = enum { @@ -78,13 +94,25 @@ const State = struct { allocator: std.mem.Allocator, page: u8 = 0, backend: Backend, + focused_action: Action = .primary, + provider: ?*NativeProvider = null, closed: bool = false, }; +const Action = enum(c_int) { + skip = 2, + back = 3, + primary = 4, + claude_code = 5, + copilot_cli = 6, + codex = 7, +}; + const class_name = std.unicode.utf8ToUtf16LeStringLiteral("GraphCodeWindowsOnboarding"); const title = std.unicode.utf8ToUtf16LeStringLiteral("Welcome to GraphCode"); const client_width: i32 = 560; const client_height: i32 = 620; +const wm_uia_focus_action: c.UINT = c.WM_APP + 60; var active = false; var active_state: State = undefined; @@ -137,6 +165,12 @@ pub fn show(parent: c.HWND, allocator: std.mem.Allocator, initial_backend: []con active = false; return error.OnboardingCreationFailed; }; + active_state.provider = gc_onboarding_uia_create(hwnd) orelse { + _ = c.DestroyWindow(hwnd); + active = false; + return error.OnboardingAccessibilityProviderCreationFailed; + }; + syncProvider(); const region = c.CreateRoundRectRgn(0, 0, width, height, 18, 18); if (region != null and c.SetWindowRgn(hwnd, region, 1) == 0) { _ = c.DeleteObject(region); @@ -157,6 +191,8 @@ pub fn show(parent: c.HWND, allocator: std.mem.Allocator, initial_backend: []con _ = c.DispatchMessageW(&message); } const backend = active_state.backend; + gc_onboarding_uia_release(active_state.provider.?); + active_state.provider = null; ModalTeardown.dismiss(hwnd, parent); active = false; return backend; @@ -180,29 +216,46 @@ fn windowProc(hwnd: c.HWND, message: c.UINT, wparam: c.WPARAM, lparam: c.LPARAM) c.WM_PAINT => { var paint_state: c.PAINTSTRUCT = undefined; const hdc = c.BeginPaint(hwnd, &paint_state); - paint(hdc, active_state.allocator, active_state.page, active_state.backend); + paint( + hdc, + active_state.allocator, + active_state.page, + active_state.backend, + active_state.focused_action, + ); _ = c.EndPaint(hwnd, &paint_state); return 0; }, + c.WM_GETOBJECT => if (active_state.provider) |provider| + return gc_onboarding_uia_get_object(hwnd, wparam, lparam, provider), + c.WM_COMMAND => { + const action = std.meta.intToEnum(Action, @as(c_int, @intCast(wparam & 0xffff))) catch + return c.DefWindowProcW(hwnd, message, wparam, lparam); + invokeAction(&active_state, action); + syncProvider(); + _ = c.InvalidateRect(hwnd, null, 0); + return 0; + }, + wm_uia_focus_action => { + const action = std.meta.intToEnum(Action, @as(c_int, @intCast(wparam & 0xffff))) catch + return c.DefWindowProcW(hwnd, message, wparam, lparam); + if (actionAvailable(active_state.page, action)) { + active_state.focused_action = action; + syncProvider(); + _ = c.InvalidateRect(hwnd, null, 0); + } + return 0; + }, c.WM_LBUTTONUP => { const point = CanvasInput.decodeMouseMessage(lparam); handleClick(hwnd, point.x, point.y); return 0; }, c.WM_KEYDOWN => { - switch (wparam) { - c.VK_ESCAPE => active_state.closed = true, - c.VK_LEFT => { - if (active_state.page > 0) active_state.page -= 1; - }, - c.VK_RIGHT, c.VK_RETURN => { - if (active_state.page + 1 < page_count) - active_state.page += 1 - else - active_state.closed = true; - }, - else => return c.DefWindowProcW(hwnd, message, wparam, lparam), - } + const shift = (@as(i32, c.GetKeyState(c.VK_SHIFT)) & 0x8000) != 0; + if (!applyKeyboard(&active_state, wparam, shift)) + return c.DefWindowProcW(hwnd, message, wparam, lparam); + syncProvider(); _ = c.InvalidateRect(hwnd, null, 0); return 0; }, @@ -215,47 +268,128 @@ fn windowProc(hwnd: c.HWND, message: c.UINT, wparam: c.WPARAM, lparam: c.LPARAM) return c.DefWindowProcW(hwnd, message, wparam, lparam); } +fn syncProvider() void { + const provider = active_state.provider orelse return; + const result = gc_onboarding_uia_update( + provider, + active_state.page, + @intFromEnum(active_state.focused_action), + @intFromEnum(active_state.backend), + ); + if (result < 0) + std.log.err("onboarding UIA update failed: 0x{x:0>8}", .{@as(u32, @bitCast(result))}); +} + +fn applyKeyboard(state: *State, key: usize, shift: bool) bool { + switch (key) { + c.VK_ESCAPE => state.closed = true, + c.VK_TAB => moveFocus(state, shift), + c.VK_LEFT => { + invokeAction(state, .back); + }, + c.VK_RIGHT => invokeAction(state, .primary), + c.VK_RETURN, c.VK_SPACE => invokeAction(state, state.focused_action), + else => return false, + } + return true; +} + +fn focusOrder(page: u8) []const Action { + return switch (page) { + 0 => &.{ .skip, .primary }, + 1, 2 => &.{ .skip, .back, .primary }, + else => &.{ .skip, .back, .claude_code, .copilot_cli, .codex, .primary }, + }; +} + +fn moveFocus(state: *State, reverse: bool) void { + const order = focusOrder(state.page); + const current = for (order, 0..) |action, index| { + if (action == state.focused_action) break index; + } else order.len - 1; + state.focused_action = if (reverse) + order[if (current == 0) order.len - 1 else current - 1] + else + order[(current + 1) % order.len]; +} + +fn actionAvailable(page: u8, action: Action) bool { + return switch (action) { + .skip, .primary => true, + .back => page > 0, + .claude_code, .copilot_cli, .codex => page == 3, + }; +} + +fn invokeAction(state: *State, action: Action) void { + if (!actionAvailable(state.page, action)) return; + state.focused_action = action; + switch (action) { + .skip => state.closed = true, + .back => { + state.page -= 1; + state.focused_action = .primary; + }, + .primary => { + if (state.page + 1 < page_count) { + state.page += 1; + state.focused_action = .primary; + } else { + state.closed = true; + } + }, + .claude_code => state.backend = .claudeCode, + .copilot_cli => state.backend = .copilotCLI, + .codex => state.backend = .codex, + } +} + fn handleClick(hwnd: c.HWND, x: i32, y: i32) void { if (applyClick(&active_state, x, y)) { + syncProvider(); _ = c.InvalidateRect(hwnd, null, 0); } } fn applyClick(state: *State, x: i32, y: i32) bool { if (inside(x, y, rect(474, 12, 540, 42))) { - state.closed = true; + invokeAction(state, .skip); return false; } if (state.page > 0 and inside(x, y, rect(20, 564, 102, 604))) { - state.page -= 1; + invokeAction(state, .back); return true; } if (inside(x, y, rect(418, 564, 540, 604))) { - if (state.page + 1 < page_count) - state.page += 1 - else - state.closed = true; + invokeAction(state, .primary); return true; } if (state.page == 3) { - if (inside(x, y, rect(58, 182, 502, 248))) state.backend = .claudeCode; - if (inside(x, y, rect(58, 258, 502, 324))) state.backend = .copilotCLI; - if (inside(x, y, rect(58, 334, 502, 400))) state.backend = .codex; + if (inside(x, y, rect(58, 182, 502, 248))) invokeAction(state, .claude_code); + if (inside(x, y, rect(58, 258, 502, 324))) invokeAction(state, .copilot_cli); + if (inside(x, y, rect(58, 334, 502, 400))) invokeAction(state, .codex); return true; } return false; } -fn paint(hdc: c.HDC, allocator: std.mem.Allocator, page: u8, backend: Backend) void { +fn paint( + hdc: c.HDC, + allocator: std.mem.Allocator, + page: u8, + backend: Backend, + focused_action: Action, +) void { fill(hdc, rect(0, 0, client_width, client_height), rgb(35, 35, 38)); text(hdc, allocator, "Skip", rect(474, 16, 540, 40), 13, rgb(160, 160, 166), c.DT_CENTER | c.DT_SINGLELINE, false); + if (focused_action == .skip) focusOutline(hdc, rect(474, 12, 540, 42)); switch (page) { 0 => paintWelcome(hdc, allocator), 1 => paintReading(hdc, allocator), 2 => paintTypes(hdc, allocator), - else => paintBackends(hdc, allocator, backend), + else => paintBackends(hdc, allocator, backend, focused_action), } - paintFooter(hdc, allocator, page); + paintFooter(hdc, allocator, page, focused_action); } fn paintWelcome(hdc: c.HDC, allocator: std.mem.Allocator) void { @@ -265,9 +399,7 @@ fn paintWelcome(hdc: c.HDC, allocator: std.mem.Allocator) void { card(hdc, allocator, rect(276, 144, 472, 204), rgb(123, 210, 130), "Fix the top crash", "RUNNING", "pass 3 - 1.42 -> 1.10", false); card(hdc, allocator, rect(78, 220, 274, 280), rgb(179, 138, 255), "Review the fix", "NEEDS YOU", "\"Ship this, or split it in two?\"", true); text(hdc, allocator, "Agents you can watch", rect(32, 330, 528, 370), 25, rgb(245, 245, 247), c.DT_CENTER | c.DT_SINGLELINE, true); - text(hdc, allocator, - "Every card is a real terminal session you can open and steer. They hand work to each other along the edges - and tell you when they need you.", - rect(48, 382, 512, 456), 14, rgb(168, 168, 174), c.DT_CENTER | c.DT_WORDBREAK, false); + text(hdc, allocator, "Every card is a real terminal session you can open and steer. They hand work to each other along the edges - and tell you when they need you.", rect(48, 382, 512, 456), 14, rgb(168, 168, 174), c.DT_CENTER | c.DT_WORDBREAK, false); } fn paintReading(hdc: c.HDC, allocator: std.mem.Allocator) void { @@ -297,17 +429,26 @@ fn paintTypes(hdc: c.HDC, allocator: std.mem.Allocator) void { card(hdc, allocator, rect(76, 370, 244, 424), rgb(90, 174, 255), "Find bugs", "RUNNING", "/loop 1h", false); line(hdc, 250, 397, 306, 397, rgb(112, 112, 118), 2); card(hdc, allocator, rect(314, 370, 482, 424), rgb(123, 210, 130), "Fix them", "IDLE", "waiting on the hand-off", false); - text(hdc, allocator, - "Wire them together by dragging from a card's + handle. Click + without dragging to grow a new loop already connected to it.", - rect(58, 446, 502, 512), 13, rgb(158, 158, 165), c.DT_CENTER | c.DT_WORDBREAK, false); + text(hdc, allocator, "Wire them together by dragging from a card's + handle. Click + without dragging to grow a new loop already connected to it.", rect(58, 446, 502, 512), 13, rgb(158, 158, 165), c.DT_CENTER | c.DT_WORDBREAK, false); } -fn paintBackends(hdc: c.HDC, allocator: std.mem.Allocator, selected: Backend) void { +fn paintBackends( + hdc: c.HDC, + allocator: std.mem.Allocator, + selected: Backend, + focused_action: Action, +) void { text(hdc, allocator, "Which agent runs them", rect(32, 64, 528, 102), 25, rgb(245, 245, 247), c.DT_CENTER | c.DT_SINGLELINE, true); text(hdc, allocator, "The default for new loops. Change it any time in Settings, or per loop.", rect(52, 108, 508, 150), 14, rgb(168, 168, 174), c.DT_CENTER | c.DT_WORDBREAK, false); backendRow(hdc, allocator, rect(58, 182, 502, 248), .claudeCode, selected, "Claude Code", "Anthropic's agent - the reference backend, fully wired.", true); backendRow(hdc, allocator, rect(58, 258, 502, 324), .copilotCLI, selected, "Copilot CLI", "GitHub's agent CLI.", false); backendRow(hdc, allocator, rect(58, 334, 502, 400), .codex, selected, "Codex", "OpenAI's agent CLI.", false); + switch (focused_action) { + .claude_code => focusOutline(hdc, rect(58, 182, 502, 248)), + .copilot_cli => focusOutline(hdc, rect(58, 258, 502, 324)), + .codex => focusOutline(hdc, rect(58, 334, 502, 400)), + else => {}, + } text(hdc, allocator, "The CLI must be installed and on your PATH - GraphCode launches it, it doesn't bundle it.", rect(72, 434, 488, 480), 12, rgb(126, 126, 133), c.DT_CENTER | c.DT_WORDBREAK, false); } @@ -333,14 +474,16 @@ fn backendRow( text(hdc, allocator, if (is_selected) "●" else "○", rect(bounds.right - 38, bounds.top + 21, bounds.right - 12, bounds.top + 47), 18, if (is_selected) rgb(10, 132, 255) else rgb(110, 110, 116), c.DT_CENTER | c.DT_SINGLELINE, false); } -fn paintFooter(hdc: c.HDC, allocator: std.mem.Allocator, page: u8) void { +fn paintFooter(hdc: c.HDC, allocator: std.mem.Allocator, page: u8, focused_action: Action) void { if (page > 0) { rounded(hdc, rect(20, 564, 102, 604), rgb(48, 48, 52), rgb(78, 78, 84), 9); text(hdc, allocator, "Back", rect(20, 576, 102, 598), 13, rgb(230, 230, 234), c.DT_CENTER | c.DT_SINGLELINE, false); + if (focused_action == .back) focusOutline(hdc, rect(20, 564, 102, 604)); } const primary = page + 1 == page_count; rounded(hdc, rect(418, 564, 540, 604), if (primary) rgb(10, 132, 255) else rgb(48, 48, 52), if (primary) rgb(10, 132, 255) else rgb(78, 78, 84), 9); text(hdc, allocator, if (primary) "Get Started" else "Continue", rect(418, 576, 540, 598), 13, rgb(245, 245, 247), c.DT_CENTER | c.DT_SINGLELINE, true); + if (focused_action == .primary) focusOutline(hdc, rect(418, 564, 540, 604)); var x: i32 = 254; for (0..page_count) |index| { const color = if (index == page) rgb(10, 132, 255) else rgb(92, 92, 98); @@ -355,6 +498,12 @@ fn paintFooter(hdc: c.HDC, allocator: std.mem.Allocator, page: u8) void { } } +fn focusOutline(hdc: c.HDC, bounds_value: c.RECT) void { + var bounds = bounds_value; + _ = c.InflateRect(&bounds, -3, -3); + _ = c.DrawFocusRect(hdc, &bounds); +} + fn typeTile(hdc: c.HDC, allocator: std.mem.Allocator, bounds: c.RECT, accent: u32, name: []const u8, detail: []const u8) void { rounded(hdc, bounds, rgb(43, 43, 47), rgb(64, 64, 69), 10); fill(hdc, rect(bounds.left, bounds.top, bounds.left + 4, bounds.bottom), accent); @@ -482,6 +631,54 @@ test "onboarding skip closes immediately" { try std.testing.expect(state.closed); } +test "onboarding keyboard focus traversal and invocation is deterministic" { + var state = State{ .allocator = std.testing.allocator, .backend = .claudeCode }; + + try std.testing.expect(applyKeyboard(&state, c.VK_TAB, false)); + try std.testing.expectEqual(Action.skip, state.focused_action); + try std.testing.expect(applyKeyboard(&state, c.VK_TAB, true)); + try std.testing.expectEqual(Action.primary, state.focused_action); + try std.testing.expect(applyKeyboard(&state, c.VK_RETURN, false)); + try std.testing.expectEqual(@as(u8, 1), state.page); + try std.testing.expectEqual(Action.primary, state.focused_action); +} + +test "onboarding keyboard backend selection remains in the deterministic focus order" { + var state = State{ + .allocator = std.testing.allocator, + .page = 3, + .backend = .claudeCode, + }; + + try std.testing.expect(applyKeyboard(&state, c.VK_TAB, false)); + try std.testing.expectEqual(Action.skip, state.focused_action); + try std.testing.expect(applyKeyboard(&state, c.VK_TAB, false)); + try std.testing.expectEqual(Action.back, state.focused_action); + try std.testing.expect(applyKeyboard(&state, c.VK_TAB, false)); + try std.testing.expectEqual(Action.claude_code, state.focused_action); + try std.testing.expect(applyKeyboard(&state, c.VK_TAB, false)); + try std.testing.expectEqual(Action.copilot_cli, state.focused_action); + try std.testing.expect(applyKeyboard(&state, c.VK_SPACE, false)); + try std.testing.expectEqual(Backend.copilotCLI, state.backend); + try std.testing.expectEqual(Action.copilot_cli, state.focused_action); +} + +test "onboarding routes WM_GETOBJECT to named Invoke-capable actions" { + const source = @embedFile("WindowsOnboarding.zig"); + const proc_start = std.mem.indexOf(u8, source, "fn windowProc") orelse + return error.OnboardingWindowProcedureMissing; + const proc_tail = source[proc_start..]; + const proc_end = std.mem.indexOf(u8, proc_tail, "fn applyKeyboard") orelse + return error.OnboardingWindowProcedureMissing; + const window_proc = proc_tail[0..proc_end]; + try std.testing.expect(std.mem.indexOf(u8, window_proc, "c.WM_GETOBJECT") != null); + try std.testing.expect(std.mem.indexOf(u8, window_proc, "gc_onboarding_uia_get_object") != null); + + const native_source = @embedFile("OnboardingAccessibilityProvider.cpp"); + try std.testing.expect(std.mem.indexOf(u8, native_source, "UIA_InvokePatternId") != null); + try std.testing.expect(std.mem.indexOf(u8, native_source, "Get Started") != null); +} + test "onboarding marker persists first-run completion" { const marker = "graphcode-onboarding-marker-test"; std.fs.cwd().deleteFile(marker) catch {}; From 8672dc2338b915a9afac0b962c0f857d0e194fb5 Mon Sep 17 00:00:00 2001 From: Colin Neilens Date: Thu, 1 Oct 2026 18:58:32 -0700 Subject: [PATCH 2/2] Normalize onboarding provider line endings Part of #556. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Signed-off-by: Colin Neilens --- .../src/OnboardingAccessibilityProvider.cpp | 1156 ++++++++--------- 1 file changed, 578 insertions(+), 578 deletions(-) diff --git a/graphcode-windows/src/OnboardingAccessibilityProvider.cpp b/graphcode-windows/src/OnboardingAccessibilityProvider.cpp index 2ea44996..0ec93bc8 100644 --- a/graphcode-windows/src/OnboardingAccessibilityProvider.cpp +++ b/graphcode-windows/src/OnboardingAccessibilityProvider.cpp @@ -1,578 +1,578 @@ -#include -#include -#include -#include -#include -#include -#include -#include - -namespace onboarding_uia { - -constexpr UINT kFocusMessage = WM_APP + 60; - -enum NodeId : int { - kRoot = 0, - kPage = 1, - kSkip = 2, - kBack = 3, - kPrimary = 4, - kClaudeCode = 5, - kCopilotCli = 6, - kCodex = 7, -}; - -struct State; - -class Node final : public IRawElementProviderSimple, - public IRawElementProviderFragment, - public IRawElementProviderFragmentRoot, - public IInvokeProvider { - public: - Node(std::shared_ptr state, int id); - - HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid, void **out) override; - ULONG STDMETHODCALLTYPE AddRef() override; - ULONG STDMETHODCALLTYPE Release() override; - HRESULT STDMETHODCALLTYPE get_ProviderOptions(ProviderOptions *value) override; - HRESULT STDMETHODCALLTYPE GetPatternProvider(PATTERNID id, IUnknown **value) override; - HRESULT STDMETHODCALLTYPE GetPropertyValue(PROPERTYID property, VARIANT *value) override; - HRESULT STDMETHODCALLTYPE get_HostRawElementProvider( - IRawElementProviderSimple **value) override; - HRESULT STDMETHODCALLTYPE Navigate( - NavigateDirection direction, - IRawElementProviderFragment **value) override; - HRESULT STDMETHODCALLTYPE GetRuntimeId(SAFEARRAY **value) override; - HRESULT STDMETHODCALLTYPE get_BoundingRectangle(UiaRect *value) override; - HRESULT STDMETHODCALLTYPE GetEmbeddedFragmentRoots(SAFEARRAY **value) override; - HRESULT STDMETHODCALLTYPE SetFocus() override; - HRESULT STDMETHODCALLTYPE get_FragmentRoot( - IRawElementProviderFragmentRoot **value) override; - HRESULT STDMETHODCALLTYPE ElementProviderFromPoint( - double x, double y, IRawElementProviderFragment **value) override; - HRESULT STDMETHODCALLTYPE GetFocus( - IRawElementProviderFragment **value) override; - HRESULT STDMETHODCALLTYPE Invoke() override; - - void shutdown(); - HRESULT update(int page, int focused, int backend); - - private: - bool availableLocked() const; - bool invokable() const; - std::vector childrenLocked() const; - Node *createRetainedLocked(int id) const; - HRESULT focusedElement(IRawElementProviderFragment **value); - - std::shared_ptr state_; - int id_; - volatile LONG refs_ = 1; -}; - -struct State { - std::mutex mutex; - HWND hwnd{}; - Node *root{}; - int page = 0; - int focused = kPrimary; - int backend = 0; - bool active = true; -}; - -static bool isAction(int id) { - return id >= kSkip && id <= kCodex; -} - -static bool hasNativeFocus(HWND hwnd) { - GUITHREADINFO info{}; - info.cbSize = sizeof(info); - return GetGUIThreadInfo(GetWindowThreadProcessId(hwnd, nullptr), &info) && - info.hwndFocus == hwnd && GetForegroundWindow() == hwnd && - IsWindowEnabled(hwnd); -} - -static bool isAvailable(int id, int page) { - if (id == kRoot || id == kPage || id == kSkip || id == kPrimary) return true; - if (id == kBack) return page > 0; - if (id >= kClaudeCode && id <= kCodex) return page == 3; - return false; -} - -static std::vector childrenForPage(int page) { - std::vector children{kPage, kSkip}; - if (page > 0) children.push_back(kBack); - if (page == 3) { - children.push_back(kClaudeCode); - children.push_back(kCopilotCli); - children.push_back(kCodex); - } - children.push_back(kPrimary); - return children; -} - -static const wchar_t *pageName(int page) { - static const wchar_t *names[] = { - L"Welcome to GraphCode: Agents you can watch", - L"How to read a loop", - L"Four kinds of loop", - L"Which agent runs them", - }; - return names[page >= 0 && page < 4 ? page : 0]; -} - -static const wchar_t *automationId(int id) { - static const wchar_t *ids[] = { - L"onboarding-root", - L"onboarding-page", - L"onboarding-skip", - L"onboarding-back", - L"onboarding-primary", - L"onboarding-backend-claude-code", - L"onboarding-backend-copilot-cli", - L"onboarding-backend-codex", - }; - return ids[id >= kRoot && id <= kCodex ? id : kRoot]; -} - -static const wchar_t *nameFor(int id, int page) { - switch (id) { - case kRoot: return L"GraphCode onboarding"; - case kPage: return pageName(page); - case kSkip: return L"Skip onboarding"; - case kBack: return L"Back"; - case kPrimary: return page == 3 ? L"Get Started" : L"Continue"; - case kClaudeCode: return L"Claude Code"; - case kCopilotCli: return L"Copilot CLI"; - case kCodex: return L"Codex"; - default: return L"GraphCode onboarding"; - } -} - -static CONTROLTYPEID controlType(int id) { - if (id == kRoot) return UIA_WindowControlTypeId; - if (id == kPage) return UIA_PaneControlTypeId; - if (id >= kClaudeCode && id <= kCodex) return UIA_RadioButtonControlTypeId; - return UIA_ButtonControlTypeId; -} - -static RECT clientBounds(int id, int page) { - switch (id) { - case kPage: return RECT{32, 56, 528, 536}; - case kSkip: return RECT{474, 12, 540, 42}; - case kBack: return RECT{20, 564, 102, 604}; - case kPrimary: return RECT{418, 564, 540, 604}; - case kClaudeCode: return RECT{58, 182, 502, 248}; - case kCopilotCli: return RECT{58, 258, 502, 324}; - case kCodex: return RECT{58, 334, 502, 400}; - default: - if (page >= 0) return RECT{0, 0, 560, 620}; - return RECT{}; - } -} - -static HRESULT focusWindow(HWND hwnd) { - const DWORD current_thread = GetCurrentThreadId(); - const DWORD window_thread = GetWindowThreadProcessId(hwnd, nullptr); - const bool attach = window_thread != 0 && window_thread != current_thread; - if (attach && !AttachThreadInput(current_thread, window_thread, TRUE)) - return HRESULT_FROM_WIN32(GetLastError()); - SetForegroundWindow(hwnd); - SetLastError(ERROR_SUCCESS); - if (::SetFocus(hwnd) == nullptr) { - const DWORD error = GetLastError(); - if (attach) AttachThreadInput(current_thread, window_thread, FALSE); - if (error != ERROR_SUCCESS) return HRESULT_FROM_WIN32(error); - } - if (attach) AttachThreadInput(current_thread, window_thread, FALSE); - return S_OK; -} - -Node::Node(std::shared_ptr state, int id) - : state_(std::move(state)), id_(id) { - if (id_ == kRoot) { - std::lock_guard lock(state_->mutex); - state_->root = this; - } -} - -HRESULT STDMETHODCALLTYPE Node::QueryInterface(REFIID iid, void **out) { - if (!out) return E_POINTER; - *out = nullptr; - if (iid == IID_IUnknown || iid == __uuidof(IRawElementProviderSimple)) - *out = static_cast(this); - else if (iid == __uuidof(IRawElementProviderFragment)) - *out = static_cast(this); - else if (iid == __uuidof(IRawElementProviderFragmentRoot) && id_ == kRoot) - *out = static_cast(this); - else if (iid == __uuidof(IInvokeProvider) && invokable()) - *out = static_cast(this); - else - return E_NOINTERFACE; - AddRef(); - return S_OK; -} - -ULONG STDMETHODCALLTYPE Node::AddRef() { - return static_cast(InterlockedIncrement(&refs_)); -} - -ULONG STDMETHODCALLTYPE Node::Release() { - const ULONG value = static_cast(InterlockedDecrement(&refs_)); - if (value == 0) delete this; - return value; -} - -HRESULT STDMETHODCALLTYPE Node::get_ProviderOptions(ProviderOptions *value) { - if (!value) return E_POINTER; - *value = static_cast( - ProviderOptions_ServerSideProvider | - ProviderOptions_UseComThreading | - ProviderOptions_ProviderOwnsSetFocus); - return S_OK; -} - -HRESULT STDMETHODCALLTYPE Node::GetPatternProvider(PATTERNID id, IUnknown **value) { - if (!value) return E_POINTER; - *value = nullptr; - if (id != UIA_InvokePatternId || !invokable()) return S_FALSE; - AddRef(); - *value = static_cast(this); - return S_OK; -} - -HRESULT STDMETHODCALLTYPE Node::GetPropertyValue(PROPERTYID property, VARIANT *value) { - if (!value) return E_POINTER; - VariantInit(value); - int page = 0; - int focused = 0; - int backend = 0; - { - std::lock_guard lock(state_->mutex); - if (!availableLocked()) return UIA_E_ELEMENTNOTAVAILABLE; - page = state_->page; - focused = state_->focused; - backend = state_->backend; - } - if (property == UIA_NamePropertyId || - property == UIA_AutomationIdPropertyId || - property == UIA_ItemStatusPropertyId) { - const wchar_t *text = L""; - if (property == UIA_NamePropertyId) text = nameFor(id_, page); - if (property == UIA_AutomationIdPropertyId) text = automationId(id_); - if (property == UIA_ItemStatusPropertyId && - id_ >= kClaudeCode && id_ <= kCodex) { - text = backend == id_ - kClaudeCode ? L"Selected" : L"Not selected"; - } - value->vt = VT_BSTR; - value->bstrVal = SysAllocString(text); - return value->bstrVal ? S_OK : E_OUTOFMEMORY; - } - if (property == UIA_ControlTypePropertyId) { - value->vt = VT_I4; - value->lVal = controlType(id_); - return S_OK; - } - if (property == UIA_IsEnabledPropertyId || - property == UIA_IsControlElementPropertyId || - property == UIA_IsContentElementPropertyId || - property == UIA_IsKeyboardFocusablePropertyId || - property == UIA_HasKeyboardFocusPropertyId) { - bool result = true; - if (property == UIA_IsKeyboardFocusablePropertyId) result = isAction(id_); - if (property == UIA_HasKeyboardFocusPropertyId) - result = focused == id_ && hasNativeFocus(state_->hwnd); - value->vt = VT_BOOL; - value->boolVal = result ? VARIANT_TRUE : VARIANT_FALSE; - return S_OK; - } - return S_FALSE; -} - -HRESULT STDMETHODCALLTYPE Node::get_HostRawElementProvider( - IRawElementProviderSimple **value) { - if (!value) return E_POINTER; - *value = nullptr; - HWND hwnd = nullptr; - { - std::lock_guard lock(state_->mutex); - if (!availableLocked()) return UIA_E_ELEMENTNOTAVAILABLE; - if (id_ != kRoot) return S_OK; - hwnd = state_->hwnd; - } - return UiaHostProviderFromHwnd(hwnd, value); -} - -HRESULT STDMETHODCALLTYPE Node::Navigate( - NavigateDirection direction, IRawElementProviderFragment **value) { - if (!value) return E_POINTER; - *value = nullptr; - Node *target = nullptr; - { - std::lock_guard lock(state_->mutex); - if (!availableLocked()) return UIA_E_ELEMENTNOTAVAILABLE; - if (direction == NavigateDirection_Parent && id_ != kRoot) { - target = createRetainedLocked(kRoot); - } else if (id_ == kRoot && - (direction == NavigateDirection_FirstChild || - direction == NavigateDirection_LastChild)) { - const auto children = childrenLocked(); - if (!children.empty()) { - target = createRetainedLocked( - direction == NavigateDirection_FirstChild - ? children.front() - : children.back()); - } - } else if (id_ != kRoot && - (direction == NavigateDirection_NextSibling || - direction == NavigateDirection_PreviousSibling)) { - const auto siblings = childrenLocked(); - const auto current = std::find(siblings.begin(), siblings.end(), id_); - if (current != siblings.end()) { - if (direction == NavigateDirection_NextSibling && - current + 1 != siblings.end()) - target = createRetainedLocked(*(current + 1)); - if (direction == NavigateDirection_PreviousSibling && - current != siblings.begin()) - target = createRetainedLocked(*(current - 1)); - } - } - } - if (target) *value = static_cast(target); - return S_OK; -} - -HRESULT STDMETHODCALLTYPE Node::GetRuntimeId(SAFEARRAY **value) { - if (!value) return E_POINTER; - *value = nullptr; - { - std::lock_guard lock(state_->mutex); - if (!availableLocked()) return UIA_E_ELEMENTNOTAVAILABLE; - } - if (id_ == kRoot) return S_OK; - *value = SafeArrayCreateVector(VT_I4, 0, 3); - if (!*value) return E_OUTOFMEMORY; - LONG values[] = {UiaAppendRuntimeId, 0x47434f, id_}; - for (LONG index = 0; index < 3; ++index) - SafeArrayPutElement(*value, &index, &values[index]); - return S_OK; -} - -HRESULT STDMETHODCALLTYPE Node::get_BoundingRectangle(UiaRect *value) { - if (!value) return E_POINTER; - HWND hwnd = nullptr; - int page = 0; - { - std::lock_guard lock(state_->mutex); - if (!availableLocked()) return UIA_E_ELEMENTNOTAVAILABLE; - hwnd = state_->hwnd; - page = state_->page; - } - RECT bounds{}; - if (id_ == kRoot) - GetClientRect(hwnd, &bounds); - else - bounds = clientBounds(id_, page); - POINT origin{0, 0}; - ClientToScreen(hwnd, &origin); - value->left = origin.x + bounds.left; - value->top = origin.y + bounds.top; - value->width = bounds.right - bounds.left; - value->height = bounds.bottom - bounds.top; - return S_OK; -} - -HRESULT STDMETHODCALLTYPE Node::GetEmbeddedFragmentRoots(SAFEARRAY **value) { - if (!value) return E_POINTER; - *value = nullptr; - return S_OK; -} - -HRESULT STDMETHODCALLTYPE Node::SetFocus() { - if (!isAction(id_)) return UIA_E_INVALIDOPERATION; - HWND hwnd = nullptr; - bool changed = false; - { - std::lock_guard lock(state_->mutex); - if (!availableLocked()) return UIA_E_ELEMENTNOTAVAILABLE; - changed = state_->focused != id_; - state_->focused = id_; - hwnd = state_->hwnd; - } - const HRESULT result = focusWindow(hwnd); - if (FAILED(result)) return result; - PostMessageW(hwnd, kFocusMessage, static_cast(id_), 0); - if (changed) - UiaRaiseAutomationEvent( - static_cast(this), - UIA_AutomationFocusChangedEventId); - return S_OK; -} - -HRESULT STDMETHODCALLTYPE Node::get_FragmentRoot( - IRawElementProviderFragmentRoot **value) { - if (!value) return E_POINTER; - *value = nullptr; - std::lock_guard lock(state_->mutex); - if (!availableLocked() || !state_->root) - return UIA_E_ELEMENTNOTAVAILABLE; - state_->root->AddRef(); - *value = static_cast(state_->root); - return S_OK; -} - -HRESULT STDMETHODCALLTYPE Node::ElementProviderFromPoint( - double x, double y, IRawElementProviderFragment **value) { - if (!value) return E_POINTER; - *value = nullptr; - HWND hwnd = nullptr; - int page = 0; - std::vector children; - { - std::lock_guard lock(state_->mutex); - if (!availableLocked()) return UIA_E_ELEMENTNOTAVAILABLE; - hwnd = state_->hwnd; - page = state_->page; - children = childrenLocked(); - } - POINT point{static_cast(x), static_cast(y)}; - ScreenToClient(hwnd, &point); - for (auto current = children.rbegin(); current != children.rend(); ++current) { - const RECT bounds = clientBounds(*current, page); - if (PtInRect(&bounds, point)) { - auto *node = new (std::nothrow) Node(state_, *current); - if (!node) return E_OUTOFMEMORY; - *value = static_cast(node); - return S_OK; - } - } - return S_OK; -} - -HRESULT STDMETHODCALLTYPE Node::GetFocus( - IRawElementProviderFragment **value) { - return focusedElement(value); -} - -HRESULT STDMETHODCALLTYPE Node::Invoke() { - HWND hwnd = nullptr; - { - std::lock_guard lock(state_->mutex); - if (!availableLocked() || !isAction(id_)) - return UIA_E_ELEMENTNOTENABLED; - hwnd = state_->hwnd; - } - return PostMessageW(hwnd, WM_COMMAND, static_cast(id_), 0) - ? S_OK - : HRESULT_FROM_WIN32(GetLastError()); -} - -void Node::shutdown() { - if (id_ != kRoot) return; - std::lock_guard lock(state_->mutex); - state_->active = false; - state_->root = nullptr; -} - -HRESULT Node::update(int page, int focused, int backend) { - bool page_changed = false; - bool focus_changed = false; - { - std::lock_guard lock(state_->mutex); - if (!state_->active) return UIA_E_ELEMENTNOTAVAILABLE; - page_changed = state_->page != page; - focus_changed = state_->focused != focused; - state_->page = page; - state_->focused = focused; - state_->backend = backend; - } - if (page_changed) - UiaRaiseStructureChangedEvent( - static_cast(this), - StructureChangeType_ChildrenInvalidated, nullptr, 0); - if (focus_changed) { - auto *focused_node = new (std::nothrow) Node(state_, focused); - if (!focused_node) return E_OUTOFMEMORY; - UiaRaiseAutomationEvent( - static_cast(focused_node), - UIA_AutomationFocusChangedEventId); - focused_node->Release(); - } - return S_OK; -} - -bool Node::availableLocked() const { - return state_->active && isAvailable(id_, state_->page); -} - -bool Node::invokable() const { - std::lock_guard lock(state_->mutex); - return availableLocked() && isAction(id_); -} - -std::vector Node::childrenLocked() const { - return childrenForPage(state_->page); -} - -Node *Node::createRetainedLocked(int id) const { - if (!isAvailable(id, state_->page)) return nullptr; - if (id == kRoot) { - if (!state_->root) return nullptr; - state_->root->AddRef(); - return state_->root; - } - return new (std::nothrow) Node(state_, id); -} - -HRESULT Node::focusedElement(IRawElementProviderFragment **value) { - if (!value) return E_POINTER; - *value = nullptr; - int focused = 0; - HWND hwnd = nullptr; - { - std::lock_guard lock(state_->mutex); - if (!availableLocked()) return UIA_E_ELEMENTNOTAVAILABLE; - focused = state_->focused; - hwnd = state_->hwnd; - if (!isAvailable(focused, state_->page)) return S_OK; - } - if (!hasNativeFocus(hwnd)) return S_OK; - auto *node = new (std::nothrow) Node(state_, focused); - if (!node) return E_OUTOFMEMORY; - *value = static_cast(node); - return S_OK; -} - -} // namespace onboarding_uia - -extern "C" IRawElementProviderSimple *gc_onboarding_uia_create(HWND hwnd) { - auto state = std::make_shared(); - state->hwnd = hwnd; - return new (std::nothrow) onboarding_uia::Node( - std::move(state), onboarding_uia::kRoot); -} - -extern "C" void gc_onboarding_uia_release( - IRawElementProviderSimple *provider) { - if (!provider) return; - auto *root = static_cast(provider); - root->shutdown(); - root->Release(); -} - -extern "C" LRESULT gc_onboarding_uia_get_object( - HWND hwnd, WPARAM wparam, LPARAM lparam, - IRawElementProviderSimple *provider) { - if (!provider || lparam != UiaRootObjectId) return 0; - return UiaReturnRawElementProvider(hwnd, wparam, lparam, provider); -} - -extern "C" HRESULT gc_onboarding_uia_update( - IRawElementProviderSimple *provider, int page, int focused, int backend) { - if (!provider || page < 0 || page > 3 || - backend < 0 || backend > 2 || - !onboarding_uia::isAvailable(focused, page) || - !onboarding_uia::isAction(focused)) - return E_INVALIDARG; - auto *root = static_cast(provider); - return root->update(page, focused, backend); -} +#include +#include +#include +#include +#include +#include +#include +#include + +namespace onboarding_uia { + +constexpr UINT kFocusMessage = WM_APP + 60; + +enum NodeId : int { + kRoot = 0, + kPage = 1, + kSkip = 2, + kBack = 3, + kPrimary = 4, + kClaudeCode = 5, + kCopilotCli = 6, + kCodex = 7, +}; + +struct State; + +class Node final : public IRawElementProviderSimple, + public IRawElementProviderFragment, + public IRawElementProviderFragmentRoot, + public IInvokeProvider { + public: + Node(std::shared_ptr state, int id); + + HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid, void **out) override; + ULONG STDMETHODCALLTYPE AddRef() override; + ULONG STDMETHODCALLTYPE Release() override; + HRESULT STDMETHODCALLTYPE get_ProviderOptions(ProviderOptions *value) override; + HRESULT STDMETHODCALLTYPE GetPatternProvider(PATTERNID id, IUnknown **value) override; + HRESULT STDMETHODCALLTYPE GetPropertyValue(PROPERTYID property, VARIANT *value) override; + HRESULT STDMETHODCALLTYPE get_HostRawElementProvider( + IRawElementProviderSimple **value) override; + HRESULT STDMETHODCALLTYPE Navigate( + NavigateDirection direction, + IRawElementProviderFragment **value) override; + HRESULT STDMETHODCALLTYPE GetRuntimeId(SAFEARRAY **value) override; + HRESULT STDMETHODCALLTYPE get_BoundingRectangle(UiaRect *value) override; + HRESULT STDMETHODCALLTYPE GetEmbeddedFragmentRoots(SAFEARRAY **value) override; + HRESULT STDMETHODCALLTYPE SetFocus() override; + HRESULT STDMETHODCALLTYPE get_FragmentRoot( + IRawElementProviderFragmentRoot **value) override; + HRESULT STDMETHODCALLTYPE ElementProviderFromPoint( + double x, double y, IRawElementProviderFragment **value) override; + HRESULT STDMETHODCALLTYPE GetFocus( + IRawElementProviderFragment **value) override; + HRESULT STDMETHODCALLTYPE Invoke() override; + + void shutdown(); + HRESULT update(int page, int focused, int backend); + + private: + bool availableLocked() const; + bool invokable() const; + std::vector childrenLocked() const; + Node *createRetainedLocked(int id) const; + HRESULT focusedElement(IRawElementProviderFragment **value); + + std::shared_ptr state_; + int id_; + volatile LONG refs_ = 1; +}; + +struct State { + std::mutex mutex; + HWND hwnd{}; + Node *root{}; + int page = 0; + int focused = kPrimary; + int backend = 0; + bool active = true; +}; + +static bool isAction(int id) { + return id >= kSkip && id <= kCodex; +} + +static bool hasNativeFocus(HWND hwnd) { + GUITHREADINFO info{}; + info.cbSize = sizeof(info); + return GetGUIThreadInfo(GetWindowThreadProcessId(hwnd, nullptr), &info) && + info.hwndFocus == hwnd && GetForegroundWindow() == hwnd && + IsWindowEnabled(hwnd); +} + +static bool isAvailable(int id, int page) { + if (id == kRoot || id == kPage || id == kSkip || id == kPrimary) return true; + if (id == kBack) return page > 0; + if (id >= kClaudeCode && id <= kCodex) return page == 3; + return false; +} + +static std::vector childrenForPage(int page) { + std::vector children{kPage, kSkip}; + if (page > 0) children.push_back(kBack); + if (page == 3) { + children.push_back(kClaudeCode); + children.push_back(kCopilotCli); + children.push_back(kCodex); + } + children.push_back(kPrimary); + return children; +} + +static const wchar_t *pageName(int page) { + static const wchar_t *names[] = { + L"Welcome to GraphCode: Agents you can watch", + L"How to read a loop", + L"Four kinds of loop", + L"Which agent runs them", + }; + return names[page >= 0 && page < 4 ? page : 0]; +} + +static const wchar_t *automationId(int id) { + static const wchar_t *ids[] = { + L"onboarding-root", + L"onboarding-page", + L"onboarding-skip", + L"onboarding-back", + L"onboarding-primary", + L"onboarding-backend-claude-code", + L"onboarding-backend-copilot-cli", + L"onboarding-backend-codex", + }; + return ids[id >= kRoot && id <= kCodex ? id : kRoot]; +} + +static const wchar_t *nameFor(int id, int page) { + switch (id) { + case kRoot: return L"GraphCode onboarding"; + case kPage: return pageName(page); + case kSkip: return L"Skip onboarding"; + case kBack: return L"Back"; + case kPrimary: return page == 3 ? L"Get Started" : L"Continue"; + case kClaudeCode: return L"Claude Code"; + case kCopilotCli: return L"Copilot CLI"; + case kCodex: return L"Codex"; + default: return L"GraphCode onboarding"; + } +} + +static CONTROLTYPEID controlType(int id) { + if (id == kRoot) return UIA_WindowControlTypeId; + if (id == kPage) return UIA_PaneControlTypeId; + if (id >= kClaudeCode && id <= kCodex) return UIA_RadioButtonControlTypeId; + return UIA_ButtonControlTypeId; +} + +static RECT clientBounds(int id, int page) { + switch (id) { + case kPage: return RECT{32, 56, 528, 536}; + case kSkip: return RECT{474, 12, 540, 42}; + case kBack: return RECT{20, 564, 102, 604}; + case kPrimary: return RECT{418, 564, 540, 604}; + case kClaudeCode: return RECT{58, 182, 502, 248}; + case kCopilotCli: return RECT{58, 258, 502, 324}; + case kCodex: return RECT{58, 334, 502, 400}; + default: + if (page >= 0) return RECT{0, 0, 560, 620}; + return RECT{}; + } +} + +static HRESULT focusWindow(HWND hwnd) { + const DWORD current_thread = GetCurrentThreadId(); + const DWORD window_thread = GetWindowThreadProcessId(hwnd, nullptr); + const bool attach = window_thread != 0 && window_thread != current_thread; + if (attach && !AttachThreadInput(current_thread, window_thread, TRUE)) + return HRESULT_FROM_WIN32(GetLastError()); + SetForegroundWindow(hwnd); + SetLastError(ERROR_SUCCESS); + if (::SetFocus(hwnd) == nullptr) { + const DWORD error = GetLastError(); + if (attach) AttachThreadInput(current_thread, window_thread, FALSE); + if (error != ERROR_SUCCESS) return HRESULT_FROM_WIN32(error); + } + if (attach) AttachThreadInput(current_thread, window_thread, FALSE); + return S_OK; +} + +Node::Node(std::shared_ptr state, int id) + : state_(std::move(state)), id_(id) { + if (id_ == kRoot) { + std::lock_guard lock(state_->mutex); + state_->root = this; + } +} + +HRESULT STDMETHODCALLTYPE Node::QueryInterface(REFIID iid, void **out) { + if (!out) return E_POINTER; + *out = nullptr; + if (iid == IID_IUnknown || iid == __uuidof(IRawElementProviderSimple)) + *out = static_cast(this); + else if (iid == __uuidof(IRawElementProviderFragment)) + *out = static_cast(this); + else if (iid == __uuidof(IRawElementProviderFragmentRoot) && id_ == kRoot) + *out = static_cast(this); + else if (iid == __uuidof(IInvokeProvider) && invokable()) + *out = static_cast(this); + else + return E_NOINTERFACE; + AddRef(); + return S_OK; +} + +ULONG STDMETHODCALLTYPE Node::AddRef() { + return static_cast(InterlockedIncrement(&refs_)); +} + +ULONG STDMETHODCALLTYPE Node::Release() { + const ULONG value = static_cast(InterlockedDecrement(&refs_)); + if (value == 0) delete this; + return value; +} + +HRESULT STDMETHODCALLTYPE Node::get_ProviderOptions(ProviderOptions *value) { + if (!value) return E_POINTER; + *value = static_cast( + ProviderOptions_ServerSideProvider | + ProviderOptions_UseComThreading | + ProviderOptions_ProviderOwnsSetFocus); + return S_OK; +} + +HRESULT STDMETHODCALLTYPE Node::GetPatternProvider(PATTERNID id, IUnknown **value) { + if (!value) return E_POINTER; + *value = nullptr; + if (id != UIA_InvokePatternId || !invokable()) return S_FALSE; + AddRef(); + *value = static_cast(this); + return S_OK; +} + +HRESULT STDMETHODCALLTYPE Node::GetPropertyValue(PROPERTYID property, VARIANT *value) { + if (!value) return E_POINTER; + VariantInit(value); + int page = 0; + int focused = 0; + int backend = 0; + { + std::lock_guard lock(state_->mutex); + if (!availableLocked()) return UIA_E_ELEMENTNOTAVAILABLE; + page = state_->page; + focused = state_->focused; + backend = state_->backend; + } + if (property == UIA_NamePropertyId || + property == UIA_AutomationIdPropertyId || + property == UIA_ItemStatusPropertyId) { + const wchar_t *text = L""; + if (property == UIA_NamePropertyId) text = nameFor(id_, page); + if (property == UIA_AutomationIdPropertyId) text = automationId(id_); + if (property == UIA_ItemStatusPropertyId && + id_ >= kClaudeCode && id_ <= kCodex) { + text = backend == id_ - kClaudeCode ? L"Selected" : L"Not selected"; + } + value->vt = VT_BSTR; + value->bstrVal = SysAllocString(text); + return value->bstrVal ? S_OK : E_OUTOFMEMORY; + } + if (property == UIA_ControlTypePropertyId) { + value->vt = VT_I4; + value->lVal = controlType(id_); + return S_OK; + } + if (property == UIA_IsEnabledPropertyId || + property == UIA_IsControlElementPropertyId || + property == UIA_IsContentElementPropertyId || + property == UIA_IsKeyboardFocusablePropertyId || + property == UIA_HasKeyboardFocusPropertyId) { + bool result = true; + if (property == UIA_IsKeyboardFocusablePropertyId) result = isAction(id_); + if (property == UIA_HasKeyboardFocusPropertyId) + result = focused == id_ && hasNativeFocus(state_->hwnd); + value->vt = VT_BOOL; + value->boolVal = result ? VARIANT_TRUE : VARIANT_FALSE; + return S_OK; + } + return S_FALSE; +} + +HRESULT STDMETHODCALLTYPE Node::get_HostRawElementProvider( + IRawElementProviderSimple **value) { + if (!value) return E_POINTER; + *value = nullptr; + HWND hwnd = nullptr; + { + std::lock_guard lock(state_->mutex); + if (!availableLocked()) return UIA_E_ELEMENTNOTAVAILABLE; + if (id_ != kRoot) return S_OK; + hwnd = state_->hwnd; + } + return UiaHostProviderFromHwnd(hwnd, value); +} + +HRESULT STDMETHODCALLTYPE Node::Navigate( + NavigateDirection direction, IRawElementProviderFragment **value) { + if (!value) return E_POINTER; + *value = nullptr; + Node *target = nullptr; + { + std::lock_guard lock(state_->mutex); + if (!availableLocked()) return UIA_E_ELEMENTNOTAVAILABLE; + if (direction == NavigateDirection_Parent && id_ != kRoot) { + target = createRetainedLocked(kRoot); + } else if (id_ == kRoot && + (direction == NavigateDirection_FirstChild || + direction == NavigateDirection_LastChild)) { + const auto children = childrenLocked(); + if (!children.empty()) { + target = createRetainedLocked( + direction == NavigateDirection_FirstChild + ? children.front() + : children.back()); + } + } else if (id_ != kRoot && + (direction == NavigateDirection_NextSibling || + direction == NavigateDirection_PreviousSibling)) { + const auto siblings = childrenLocked(); + const auto current = std::find(siblings.begin(), siblings.end(), id_); + if (current != siblings.end()) { + if (direction == NavigateDirection_NextSibling && + current + 1 != siblings.end()) + target = createRetainedLocked(*(current + 1)); + if (direction == NavigateDirection_PreviousSibling && + current != siblings.begin()) + target = createRetainedLocked(*(current - 1)); + } + } + } + if (target) *value = static_cast(target); + return S_OK; +} + +HRESULT STDMETHODCALLTYPE Node::GetRuntimeId(SAFEARRAY **value) { + if (!value) return E_POINTER; + *value = nullptr; + { + std::lock_guard lock(state_->mutex); + if (!availableLocked()) return UIA_E_ELEMENTNOTAVAILABLE; + } + if (id_ == kRoot) return S_OK; + *value = SafeArrayCreateVector(VT_I4, 0, 3); + if (!*value) return E_OUTOFMEMORY; + LONG values[] = {UiaAppendRuntimeId, 0x47434f, id_}; + for (LONG index = 0; index < 3; ++index) + SafeArrayPutElement(*value, &index, &values[index]); + return S_OK; +} + +HRESULT STDMETHODCALLTYPE Node::get_BoundingRectangle(UiaRect *value) { + if (!value) return E_POINTER; + HWND hwnd = nullptr; + int page = 0; + { + std::lock_guard lock(state_->mutex); + if (!availableLocked()) return UIA_E_ELEMENTNOTAVAILABLE; + hwnd = state_->hwnd; + page = state_->page; + } + RECT bounds{}; + if (id_ == kRoot) + GetClientRect(hwnd, &bounds); + else + bounds = clientBounds(id_, page); + POINT origin{0, 0}; + ClientToScreen(hwnd, &origin); + value->left = origin.x + bounds.left; + value->top = origin.y + bounds.top; + value->width = bounds.right - bounds.left; + value->height = bounds.bottom - bounds.top; + return S_OK; +} + +HRESULT STDMETHODCALLTYPE Node::GetEmbeddedFragmentRoots(SAFEARRAY **value) { + if (!value) return E_POINTER; + *value = nullptr; + return S_OK; +} + +HRESULT STDMETHODCALLTYPE Node::SetFocus() { + if (!isAction(id_)) return UIA_E_INVALIDOPERATION; + HWND hwnd = nullptr; + bool changed = false; + { + std::lock_guard lock(state_->mutex); + if (!availableLocked()) return UIA_E_ELEMENTNOTAVAILABLE; + changed = state_->focused != id_; + state_->focused = id_; + hwnd = state_->hwnd; + } + const HRESULT result = focusWindow(hwnd); + if (FAILED(result)) return result; + PostMessageW(hwnd, kFocusMessage, static_cast(id_), 0); + if (changed) + UiaRaiseAutomationEvent( + static_cast(this), + UIA_AutomationFocusChangedEventId); + return S_OK; +} + +HRESULT STDMETHODCALLTYPE Node::get_FragmentRoot( + IRawElementProviderFragmentRoot **value) { + if (!value) return E_POINTER; + *value = nullptr; + std::lock_guard lock(state_->mutex); + if (!availableLocked() || !state_->root) + return UIA_E_ELEMENTNOTAVAILABLE; + state_->root->AddRef(); + *value = static_cast(state_->root); + return S_OK; +} + +HRESULT STDMETHODCALLTYPE Node::ElementProviderFromPoint( + double x, double y, IRawElementProviderFragment **value) { + if (!value) return E_POINTER; + *value = nullptr; + HWND hwnd = nullptr; + int page = 0; + std::vector children; + { + std::lock_guard lock(state_->mutex); + if (!availableLocked()) return UIA_E_ELEMENTNOTAVAILABLE; + hwnd = state_->hwnd; + page = state_->page; + children = childrenLocked(); + } + POINT point{static_cast(x), static_cast(y)}; + ScreenToClient(hwnd, &point); + for (auto current = children.rbegin(); current != children.rend(); ++current) { + const RECT bounds = clientBounds(*current, page); + if (PtInRect(&bounds, point)) { + auto *node = new (std::nothrow) Node(state_, *current); + if (!node) return E_OUTOFMEMORY; + *value = static_cast(node); + return S_OK; + } + } + return S_OK; +} + +HRESULT STDMETHODCALLTYPE Node::GetFocus( + IRawElementProviderFragment **value) { + return focusedElement(value); +} + +HRESULT STDMETHODCALLTYPE Node::Invoke() { + HWND hwnd = nullptr; + { + std::lock_guard lock(state_->mutex); + if (!availableLocked() || !isAction(id_)) + return UIA_E_ELEMENTNOTENABLED; + hwnd = state_->hwnd; + } + return PostMessageW(hwnd, WM_COMMAND, static_cast(id_), 0) + ? S_OK + : HRESULT_FROM_WIN32(GetLastError()); +} + +void Node::shutdown() { + if (id_ != kRoot) return; + std::lock_guard lock(state_->mutex); + state_->active = false; + state_->root = nullptr; +} + +HRESULT Node::update(int page, int focused, int backend) { + bool page_changed = false; + bool focus_changed = false; + { + std::lock_guard lock(state_->mutex); + if (!state_->active) return UIA_E_ELEMENTNOTAVAILABLE; + page_changed = state_->page != page; + focus_changed = state_->focused != focused; + state_->page = page; + state_->focused = focused; + state_->backend = backend; + } + if (page_changed) + UiaRaiseStructureChangedEvent( + static_cast(this), + StructureChangeType_ChildrenInvalidated, nullptr, 0); + if (focus_changed) { + auto *focused_node = new (std::nothrow) Node(state_, focused); + if (!focused_node) return E_OUTOFMEMORY; + UiaRaiseAutomationEvent( + static_cast(focused_node), + UIA_AutomationFocusChangedEventId); + focused_node->Release(); + } + return S_OK; +} + +bool Node::availableLocked() const { + return state_->active && isAvailable(id_, state_->page); +} + +bool Node::invokable() const { + std::lock_guard lock(state_->mutex); + return availableLocked() && isAction(id_); +} + +std::vector Node::childrenLocked() const { + return childrenForPage(state_->page); +} + +Node *Node::createRetainedLocked(int id) const { + if (!isAvailable(id, state_->page)) return nullptr; + if (id == kRoot) { + if (!state_->root) return nullptr; + state_->root->AddRef(); + return state_->root; + } + return new (std::nothrow) Node(state_, id); +} + +HRESULT Node::focusedElement(IRawElementProviderFragment **value) { + if (!value) return E_POINTER; + *value = nullptr; + int focused = 0; + HWND hwnd = nullptr; + { + std::lock_guard lock(state_->mutex); + if (!availableLocked()) return UIA_E_ELEMENTNOTAVAILABLE; + focused = state_->focused; + hwnd = state_->hwnd; + if (!isAvailable(focused, state_->page)) return S_OK; + } + if (!hasNativeFocus(hwnd)) return S_OK; + auto *node = new (std::nothrow) Node(state_, focused); + if (!node) return E_OUTOFMEMORY; + *value = static_cast(node); + return S_OK; +} + +} // namespace onboarding_uia + +extern "C" IRawElementProviderSimple *gc_onboarding_uia_create(HWND hwnd) { + auto state = std::make_shared(); + state->hwnd = hwnd; + return new (std::nothrow) onboarding_uia::Node( + std::move(state), onboarding_uia::kRoot); +} + +extern "C" void gc_onboarding_uia_release( + IRawElementProviderSimple *provider) { + if (!provider) return; + auto *root = static_cast(provider); + root->shutdown(); + root->Release(); +} + +extern "C" LRESULT gc_onboarding_uia_get_object( + HWND hwnd, WPARAM wparam, LPARAM lparam, + IRawElementProviderSimple *provider) { + if (!provider || lparam != UiaRootObjectId) return 0; + return UiaReturnRawElementProvider(hwnd, wparam, lparam, provider); +} + +extern "C" HRESULT gc_onboarding_uia_update( + IRawElementProviderSimple *provider, int page, int focused, int backend) { + if (!provider || page < 0 || page > 3 || + backend < 0 || backend > 2 || + !onboarding_uia::isAvailable(focused, page) || + !onboarding_uia::isAction(focused)) + return E_INVALIDARG; + auto *root = static_cast(provider); + return root->update(page, focused, backend); +}