// Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // SPDX-License-Identifier: GPL-3.0-or-later package atspi /* #include "adapter_native.h" */ import "C" import ( "context" "errors" "fmt" "runtime/cgo" "strings" "time" "unsafe" ) func geometryFromNative(ctx context.Context, source *C.AtspiAccessible) (Geometry, error) { return geometryFromNativeWithBounds(ctx, source, true) } func geometryFromNativeWithBounds(ctx context.Context, source *C.AtspiAccessible, captureBounds bool) (Geometry, error) { geometry := Geometry{} if err := ctx.Err(); err != nil { return geometry, err } states := C.atspi_accessible_get_state_set(source) if states == nil { return geometry, ctx.Err() } geometry.VisibilityKnown = true geometry.Visible = C.atspi_state_set_contains(states, C.ATSPI_STATE_VISIBLE) != 0 geometry.Showing = C.atspi_state_set_contains(states, C.ATSPI_STATE_SHOWING) != 0 C.g_object_unref(C.gpointer(states)) // libatspi specifies that component bounds are meaningful only when both // states are present. Avoid querying hidden/offscreen components. if !captureBounds || !geometry.Visible || !geometry.Showing { return geometry, ctx.Err() } if err := ctx.Err(); err != nil { return geometry, err } component := C.atspi_accessible_get_component_iface(source) if component == nil { return geometry, ctx.Err() } defer C.g_object_unref(C.gpointer(component)) if err := ctx.Err(); err != nil { return geometry, err } var nativeErr *C.GError bounds := C.atspi_component_get_extents(component, C.ATSPI_COORD_TYPE_SCREEN, &nativeErr) if bounds != nil { defer C.g_free(C.gpointer(bounds)) } if nativeErr != nil { C.g_error_free(nativeErr) return geometry, ctx.Err() } if bounds != nil { rectangle := Rectangle{X: int32(bounds.x), Y: int32(bounds.y), Width: int32(bounds.width), Height: int32(bounds.height)} if rectangle.Valid() { geometry.Bounds = rectangle geometry.BoundsKnown = true } } return geometry, ctx.Err() } func admitNativeChild(scheduled map[ObjectID]struct{}, childID ObjectID) bool { if _, duplicate := scheduled[childID]; duplicate { return false } scheduled[childID] = struct{}{} return true } type nativeTreeCache struct { nodes map[ObjectID]treeEntry sources map[ObjectID]*C.AtspiAccessible } // captureNativeTree copies the desktop or, when reviewBusName is set, just the // focused application's subtree. The latter keeps flat review independent of // unrelated AT-SPI providers that can be slow or transiently unavailable. func captureNativeTree(ctx context.Context, root *C.AtspiAccessible, reviewBusName string, document, documentText bool, retained map[ObjectID]*C.AtspiAccessible, cache nativeTreeCache) (ObjectID, []TreeNode, error) { trace := capture_diagnostics(ctx) trace.begin_stage("tree-setup", ObjectID{}, "") reviewOnly := !document && reviewBusName != "" rootID := nativeObjectID(root) if !rootID.Valid() { C.g_object_unref(C.gpointer(root)) return ObjectID{}, nil, fmt.Errorf("%w: native root identity is invalid", ErrInvalidTree) } type pendingNode struct { source *C.AtspiAccessible clip Rectangle clipped bool clipChanged bool parentIndex int } pending := []pendingNode{{source: root, parentIndex: -1}} textBytes := 0 scheduled := map[ObjectID]struct{}{rootID: {}} nodes := make([]TreeNode, 0, 128) // Embedded frames can belong to another AT-SPI process. Application names // are stable within one bus, including during flat review of a large page. applicationNames := make(map[string]string) releasePending := func() { for _, item := range pending { C.g_object_unref(C.gpointer(item.source)) } } for len(pending) != 0 { if err := ctx.Err(); err != nil { releasePending() return ObjectID{}, nil, err } last := len(pending) - 1 item := pending[last] source := item.source pending = pending[:last] sourceID := nativeObjectID(source) cached, known := cache.nodes[sourceID] refresh := !known || cached.snapshotInvalid || cached.textInvalid || cached.geometryInvalid || cached.childrenInvalid || cached.childrenUnavailable snapshot := cached.snapshot trace.begin_stage("snapshot", sourceID, snapshot.Role) if refresh { // Only changed/new objects require synchronous provider queries. // Reusing live siblings matches Python's event-driven web cache and // prevents a small insertion from repeatedly timing out on a big page. // New document and flat-review nodes already have libatspi's // cached child lists. Clearing them forces another child-count // request even for a cached leaf, which can fail on a busy provider. // Known dirty entries still need a fresh provider snapshot. Preserve // the existing full refresh for desktop/object capture. if known || (!document && !reviewOnly) { C.atspi_accessible_clear_cache_single(source) } else if reviewOnly { // A browse scroll can leave SHOWING cached from the previous // viewport without a state-change event for each descendant. // Refresh states on entry to visual capture, while preserving // cached children and interfaces for slow native providers. source.cached_properties &^= C.ATSPI_CACHE_STATES } // Navigation refreshes sound geometry only for the announced target, // after scrolling. Capturing it for every descendant repeats native // ancestor walks and can exhaust the document capture deadline. applicationName, knownApplication := applicationNames[nativeObjectID(source).BusName] if knownApplication { snapshot = snapshotFromNativeWithOptions(source, !document, false, document, false, false) snapshot.ApplicationName = applicationName } else { snapshot = snapshotFromNativeWithCaret(source, !document, false, document, false) } } trace.progress(snapshot.Role, len(nodes), len(pending)+1, textBytes) if _, knownApplication := applicationNames[snapshot.BusName]; !knownApplication { applicationNames[snapshot.BusName] = snapshot.ApplicationName } if snapshot.Defunct { if (document || reviewOnly) && item.parentIndex >= 0 { // Providers can retire a descendant after exposing it in its // parent's child list. Drop that edge and keep the live window. children := nodes[item.parentIndex].Children for index, child := range children { if child == snapshot.ID() { nodes[item.parentIndex].Children = append(children[:index], children[index+1:]...) break } } C.g_object_unref(C.gpointer(source)) continue } C.g_object_unref(C.gpointer(source)) releasePending() return ObjectID{}, nil, fmt.Errorf("%w: bus %q path %q", ErrDeadObject, snapshot.BusName, snapshot.Path) } if retained != nil { retained[snapshot.ID()] = (*C.AtspiAccessible)(C.g_object_ref(C.gpointer(source))) } trace.begin_stage("geometry", snapshot.ID(), snapshot.Role) geometry := cached.geometry var geometryErr error if refresh { geometry, geometryErr = geometryFromNativeWithBounds(ctx, source, !document || documentText) } if geometryErr != nil { C.g_object_unref(C.gpointer(source)) releasePending() return ObjectID{}, nil, geometryErr } clip, clipped := item.clip, item.clipped if geometry.BoundsKnown { if clipped { clip, _ = clip.Intersection(geometry.Bounds) } else { clip = geometry.Bounds clipped = true } } else if geometry.VisibilityKnown && (!geometry.Visible || !geometry.Showing) && snapshot.Role != "application" && snapshot.Role != "desktop frame" { clip, clipped = Rectangle{}, true } childClip, childClipped := clip, clipped if snapshot.Role == "page tab" && geometry.Visible && geometry.Showing { // A tab's bounds describe its header, while its accessible children // can be the displayed page below it. Preserve the parent viewport. childClip, childClipped = item.clip, item.clipped } trace.begin_stage("visible-text", snapshot.ID(), snapshot.Role) text := VisibleText{Limited: true, Issue: TextTreeLimit} if (!document || documentText) && textBytes < maxTreeTextBytes { if known && !cached.textInvalid && !cached.geometryInvalid && !item.clipChanged { text = cached.text } else { var textErr error text, textErr = textFromNative(ctx, source, geometry, snapshot, clip) if textErr != nil { C.g_object_unref(C.gpointer(source)) releasePending() return ObjectID{}, nil, textErr } } size := 0 for _, r := range text.Ranges { size += len(r.Text) } if textBytes+size > maxTreeTextBytes { text = VisibleText{Limited: true, Issue: TextTreeLimit} trace.text_issue(TextTreeLimit) } else { textBytes += size } } trace.begin_stage("table-viewport", snapshot.ID(), snapshot.Role) // Visual review cannot use descendants of hidden controls. Inactive // notebook pages and closed GTK combo menus can expose thousands of // children; traversing them can exhaust the entire capture deadline. // Keep semantic document and desktop/object capture complete. captureChildren := !reviewOnly || !geometry.VisibilityKnown || (geometry.Visible && geometry.Showing) || snapshot.Role == "application" || snapshot.Role == "desktop frame" // A visibility change can reveal children omitted by an earlier review // capture, even when the provider emits no children-changed event. cachedChildren := known && !cached.childrenInvalid && !cached.childrenUnavailable && (!reviewOnly || !cached.geometryInvalid) childrenUnavailable := false for _, id := range cached.children { if cache.sources[id] == nil { cachedChildren = false break } } var childCountError *C.GError childCount := len(cached.children) var tableChildren []*C.AtspiAccessible tableViewport := false if reviewOnly && captureChildren { var tableErr error tableChildren, tableViewport, tableErr = native_review_table_children(ctx, source, clip) if tableErr != nil { C.g_object_unref(C.gpointer(source)) releasePending() return ObjectID{}, nil, fmt.Errorf("%w (bus %q path %q role %q)", tableErr, snapshot.BusName, snapshot.Path, snapshot.Role) } // The pending stack takes its own reference to each admitted cell. defer func(children []*C.AtspiAccessible) { for _, child := range children { C.g_object_unref(C.gpointer(child)) } }(tableChildren) } trace.begin_stage("child-count", snapshot.ID(), snapshot.Role) if !captureChildren { childCount = 0 } else if tableViewport { childCount = len(tableChildren) } else if !cachedChildren { // Snapshot refresh above already cleared dirty entries. Clearing // again discards the state and interface data just fetched. childCount = int(C.atspi_accessible_get_child_count(source, &childCountError)) } if childCountError != nil { err := fmt.Errorf("%w (bus %q path %q role %q captured_nodes %d)", nativeError("query AT-SPI child count", childCountError), snapshot.BusName, snapshot.Path, snapshot.Role, len(nodes)) C.g_error_free(childCountError) if !reviewOnly || item.parentIndex < 0 || ctx.Err() != nil { C.g_object_unref(C.gpointer(source)) releasePending() return ObjectID{}, nil, err } childCount, childrenUnavailable = 0, true } if childCount < 0 { if reviewOnly && item.parentIndex >= 0 && ctx.Err() == nil { childCount, childrenUnavailable = 0, true } else { C.g_object_unref(C.gpointer(source)) releasePending() return ObjectID{}, nil, fmt.Errorf("query AT-SPI child count: negative result (bus %q path %q role %q captured_nodes %d)", snapshot.BusName, snapshot.Path, snapshot.Role, len(nodes)) } } if reviewBusName == "" && len(scheduled)+childCount > maxTreeNodes { C.g_object_unref(C.gpointer(source)) releasePending() return ObjectID{}, nil, fmt.Errorf("%w: native tree exceeds limit %d", ErrInvalidTree, maxTreeNodes) } trace.begin_stage("child-enumeration", snapshot.ID(), snapshot.Role) children := make([]ObjectID, 0, childCount) for index := 0; index < childCount; index++ { if err := ctx.Err(); err != nil { C.g_object_unref(C.gpointer(source)) releasePending() return ObjectID{}, nil, err } var childError *C.GError var child *C.AtspiAccessible if tableViewport { child = (*C.AtspiAccessible)(C.g_object_ref(C.gpointer(tableChildren[index]))) } else if cachedChildren { child = (*C.AtspiAccessible)(C.g_object_ref(C.gpointer(cache.sources[cached.children[index]]))) } else { child = C.atspi_accessible_get_child_at_index(source, C.gint(index), &childError) } if childError != nil { err := nativeError("query AT-SPI child", childError) C.g_error_free(childError) if child != nil { C.g_object_unref(C.gpointer(child)) } if reviewOnly && ctx.Err() == nil { childrenUnavailable = true continue } C.g_object_unref(C.gpointer(source)) releasePending() return ObjectID{}, nil, err } if child == nil { // Child lists can change between the count and lookup calls. Match // Python Cthulhu's traversal policy: omit an unavailable child while // retaining the rest of the current parent snapshot. A missing child // must not prevent flat review of an otherwise live focused window. continue } childID := nativeObjectID(child) if !childID.Valid() { C.g_object_unref(C.gpointer(child)) C.g_object_unref(C.gpointer(source)) releasePending() return ObjectID{}, nil, fmt.Errorf("%w: native child identity is invalid", ErrInvalidTree) } if reviewBusName != "" && childID.BusName != reviewBusName { C.g_object_unref(C.gpointer(child)) continue } if len(scheduled) >= maxTreeNodes { C.g_object_unref(C.gpointer(child)) C.g_object_unref(C.gpointer(source)) releasePending() return ObjectID{}, nil, fmt.Errorf("%w: native tree exceeds limit %d", ErrInvalidTree, maxTreeNodes) } if !admitNativeChild(scheduled, childID) { // Chromium can transiently expose the same accessible through more // than one child edge while replacing document content. Python's // flat-review traversal tolerates that provider defect. Keep the // first edge so the owned snapshot remains a proper tree rather than // rejecting the entire active window. C.g_object_unref(C.gpointer(child)) continue } children = append(children, childID) pending = append(pending, pendingNode{source: child, clip: childClip, clipped: childClipped, clipChanged: item.clipChanged || !known || cached.geometryInvalid, parentIndex: len(nodes)}) } if document && refresh { snapshot.TextChildren = nativeTextChildren(ctx, source, snapshot.CharacterCount, children) } nodes = append(nodes, TreeNode{Snapshot: snapshot, Children: children, Geometry: geometry, Text: text, ChildrenUnavailable: childrenUnavailable}) trace.progress(snapshot.Role, len(nodes), len(pending), textBytes) C.g_object_unref(C.gpointer(source)) } return rootID, nodes, nil } //export cthulhuAtspiTreeCaptureCallback func cthulhuAtspiTreeCaptureCallback(userData unsafe.Pointer) { if userData == nil { return } callbackHandle := cgo.Handle(C.cthulhu_callback_value((*C.cthulhu_callback_data)(userData))) adapter, ok := callbackHandle.Value().(*Adapter) if !ok { return } request := <-adapter.treeRequests capture_diagnostics(request.ctx).begin_stage("callback-setup", request.reviewFocus, "") adapter.applySecureReset() if request.ctx.Err() != nil { request.result <- treeCaptureResult{err: request.ctx.Err()} return } if adapter.secure.Load() { request.result <- treeCaptureResult{err: errors.New("screen lock active")} return } if request.transferItem { item, err := adapter.nativeTransferItem(request.ctx, request.reviewFocus, request.positionOffset) request.result <- treeCaptureResult{item: item, err: err} } else if request.listActions || request.actionTarget != nil { object := request.reviewFocus if request.actionTarget != nil { object = request.actionTarget.Object } actions, err := adapter.native_actions(request.ctx, object, request.actionTarget, request.actionName) request.result <- treeCaptureResult{actions: actions, err: err} } else if request.ocrGeometry { geometry, err := adapter.nativeOCRGeometry(request.ctx, request.reviewFocus, request.ocrZone) request.result <- treeCaptureResult{ocr: geometry, err: err} } else if request.pointReview { point, err := adapter.nativeReviewAtPoint(request.ctx, request.pointX, request.pointY, request.pointPID) request.result <- treeCaptureResult{point: point, err: err} } else if request.brailleRoute != nil { err := adapter.routeNativeBraille(request.ctx, *request.brailleRoute) request.result <- treeCaptureResult{err: err} } else if request.setFocusedCaret { err := adapter.setNativeFocusedTextCaret(request.ctx, request.reviewFocus, request.positionOffset) request.result <- treeCaptureResult{err: err} } else if request.readFocusedLine { content, err := adapter.read_native_focused_line(request.ctx, request.reviewFocus) request.result <- treeCaptureResult{content: content, err: err} } else if request.readFocusedText { content, err := adapter.readNativeFocusedText(request.ctx, request.reviewFocus) request.result <- treeCaptureResult{content: content, err: err} } else if request.readAttributes { attributes, err := adapter.readNativeCharacterAttributes(request.ctx, request.reviewFocus) request.result <- treeCaptureResult{attributes: attributes, err: err} } else if request.contextOnly { owner, err := adapter.nativeDocumentContext(request.ctx, request.reviewFocus) request.result <- treeCaptureResult{owner: owner, err: err} } else if request.caretObject.Valid() { caret, err := adapter.readNativeDocumentCaret(request.ctx, request.caretObject, request.caretOffset, request.caretUnit) request.result <- treeCaptureResult{caret: caret, err: err} } else if request.activate { err := adapter.activateNativeDocumentObject(request.ctx, request.position, request.reviewFocus) request.result <- treeCaptureResult{err: err} } else if request.soundObjects[0].Valid() { geometry, err := adapter.captureNativeDocumentSoundGeometry(request.ctx, request.soundObjects) request.result <- treeCaptureResult{soundGeometry: geometry, err: err} } else if request.position.Valid() { err := adapter.setNativeDocumentPosition(request.ctx, request.position, request.reviewFocus, request.positionOffset, request.usePositionOffset) request.result <- treeCaptureResult{err: err} } else { tree, err := adapter.captureDesktop(request.ctx, request.reviewFocus, request.document, request.documentText) request.result <- treeCaptureResult{tree: tree, err: err} } } func (adapter *Adapter) applyTreeInvalidation(eventType string, event *C.AtspiEvent) bool { if isDocumentLifecycleEvent(eventType) { clear(adapter.navigationEcho) sourceID := nativeObjectID(event.source) adapter.treeMutex.Lock() documentTree := adapter.documentTree adapter.treeMutex.Unlock() replacement := false if documentTree != nil { root, err := documentTree.Root() if err == nil && root.ID().BusName == sourceID.BusName { _, err = documentTree.Reference(sourceID) replacement = errors.Is(err, ErrObjectNotFound) } } adapter.invalidateTreeEvent(eventType, 0, sourceID, ObjectID{}) // A replacement page can have a different accessible path from the // captured document. In that case invalidating the event source is a // no-op. Retire the old tree only when this is the focused document; // background tabs must not displace the active browse position. if replacement && len(adapter.nativeFocus) != 0 { focus := adapter.nativeFocus[len(adapter.nativeFocus)-1].id ctx, cancel := context.WithTimeout(context.Background(), 250*time.Millisecond) active, err := adapter.nativeDocumentRoot(ctx, focus) cancel() if err == nil { activeID := nativeObjectID(active) C.g_object_unref(C.gpointer(active)) adapter.invalidateReplacedDocument(sourceID, activeID) } } return false } childrenAdded := strings.HasPrefix(eventType, ChildrenAddedEvent) childrenRemoved := strings.HasPrefix(eventType, ChildrenRemovedEvent) textChanged := strings.HasPrefix(eventType, TextInsertedEvent) || strings.HasPrefix(eventType, TextDeletedEvent) || eventType == TextBoundsChangedEvent || eventType == TextAttributesChangedEvent || eventType == CaretEvent if textChanged { adapter.invalidateTreeEvent(eventType, int(C.cthulhu_event_detail1(event)), nativeObjectID(event.source), ObjectID{}) return eventType == TextBoundsChangedEvent || eventType == TextAttributesChangedEvent } geometryChanged := eventType == BoundsChangedEvent || eventType == VisibleStateEvent || eventType == ShowingStateEvent semanticChanged := semanticSnapshotChanged(eventType) if !childrenAdded && !childrenRemoved && eventType != DefunctStateEvent && !geometryChanged && !semanticChanged { return false } sourceID := nativeObjectID(event.source) if semanticChanged { adapter.invalidateTreeEvent(eventType, int(C.cthulhu_event_detail1(event)), sourceID, ObjectID{}) return false } childID := ObjectID{} if childrenRemoved { child := C.cthulhu_event_active_descendant(event) childID = nativeObjectID(child) } detail1 := int(C.cthulhu_event_detail1(event)) // Setting Chromium's caret can retire the target or one of its descendants // and immediately replace it with a new provider object. That defunct event // is part of the navigation echo, but its native object is still dead. // Retain the document and live siblings while refreshing the affected // parent's children, so navigation can recover the replacement. navigationDefunct := eventType == DefunctStateEvent && detail1 != 0 && adapter.navigationEcho[sourceID] != 0 adapter.invalidateTreeEventWithPolicy(eventType, detail1, sourceID, childID, navigationDefunct) if navigationDefunct { delete(adapter.navigationEcho, sourceID) } return true } func (adapter *Adapter) invalidateReplacedDocument(sourceID, activeID ObjectID) { if !sourceID.Valid() || sourceID != activeID { return } adapter.treeMutex.Lock() trees := []*AccessibleTree{adapter.tree, adapter.documentTree, adapter.documentTextTree} adapter.treeMutex.Unlock() for _, tree := range trees { if tree == nil { continue } root, err := tree.Root() if err != nil || root.ID().BusName != sourceID.BusName { continue } if _, err := tree.Reference(sourceID); err == nil { continue } adapter.treeInvalidations.Add(uint64(tree.Invalidate(root.ID()))) } } func (adapter *Adapter) invalidateTreeEvent(eventType string, detail1 int, sourceID, childID ObjectID) { adapter.invalidateTreeEventWithPolicy(eventType, detail1, sourceID, childID, false) } func (adapter *Adapter) invalidateTreeEventWithPolicy(eventType string, detail1 int, sourceID, childID ObjectID, navigationDefunct bool) { if eventType == DefunctStateEvent && detail1 == 0 { return } if eventType != CaretEvent { // Background providers cannot change the owned document's text lines. for id := range adapter.documentLineCache { if id.BusName == sourceID.BusName { delete(adapter.documentLineCache, id) } } } adapter.treeMutex.Lock() trees := []*AccessibleTree{adapter.tree, adapter.documentTree, adapter.documentTextTree} adapter.treeMutex.Unlock() for index, tree := range trees { if navigationDefunct && index == 1 && tree != nil { tree.mu.RLock() entry := tree.nodes[sourceID] tree.mu.RUnlock() adapter.treeInvalidations.Add(uint64(tree.Invalidate(sourceID))) if entry.hasParent { adapter.treeInvalidations.Add(uint64(tree.MarkChildrenChanged(entry.parent))) } continue } // Structural matching consumes the semantic document snapshot. Other // owned trees remain usable for object/flat review until their own // established text, geometry, child, or lifecycle invalidation. if semanticSnapshotChanged(eventType) && index != 1 { continue } // Caret position and viewport bounds do not change semantic order, // names or hidden state. Flat review still needs these invalidations. if index == 1 && (eventType == CaretEvent || eventType == BoundsChangedEvent || eventType == TextBoundsChangedEvent || eventType == ShowingStateEvent) { continue } // The visual-line tree receives layout, visibility, and text // invalidations. A caret event supplies its current offset separately, // so it does not make otherwise-current line geometry stale. if index == 2 && eventType == CaretEvent { continue } if index == 1 && tree != nil && (strings.HasPrefix(eventType, ChildrenAddedEvent) || strings.HasPrefix(eventType, ChildrenRemovedEvent) && !childID.Valid()) { adapter.treeInvalidations.Add(uint64(tree.MarkChildrenChanged(sourceID))) continue } adapter.invalidateCapturedTree(tree, eventType, detail1, sourceID, childID) } } func (adapter *Adapter) invalidateCapturedTree(tree *AccessibleTree, eventType string, detail1 int, sourceID, childID ObjectID) { if tree == nil { return } removed := 0 switch { case isDocumentLifecycleEvent(eventType): // Browsers can reuse an accessible path when replacing a document. // Retire its generation, including all descendants, before recapture. removed = tree.Invalidate(sourceID) case strings.HasPrefix(eventType, TextInsertedEvent) || strings.HasPrefix(eventType, TextDeletedEvent) || eventType == TextBoundsChangedEvent || eventType == TextAttributesChangedEvent || eventType == CaretEvent: removed = tree.InvalidateText(sourceID) case eventType == BoundsChangedEvent || eventType == VisibleStateEvent || eventType == ShowingStateEvent: removed = tree.InvalidateGeometry(sourceID) case eventType == DefunctStateEvent: removed = tree.Invalidate(sourceID) case semanticSnapshotChanged(eventType): // Refresh changed semantics without discarding document/caret identities. // Events outside this tree remain a no-op. removed = tree.InvalidateSnapshot(sourceID) case strings.HasPrefix(eventType, ChildrenAddedEvent): removed = tree.InvalidateChildren(sourceID) case strings.HasPrefix(eventType, ChildrenRemovedEvent): if childID.Valid() { removed = tree.Invalidate(childID) } else { removed = tree.InvalidateChildren(sourceID) } } adapter.treeInvalidations.Add(uint64(removed)) } func semanticSnapshotChanged(eventType string) bool { if eventType == AccessibleNameEvent || eventType == ValueChangedEvent || eventType == AccessibleValueEvent { return true } switch eventType { case "object:state-changed:checked", "object:state-changed:expanded", "object:state-changed:indeterminate", "object:state-changed:invalid-entry", "object:state-changed:pressed", "object:state-changed:read-only", "object:state-changed:required", "object:state-changed:selected", "object:state-changed:sensitive", "object:state-changed:visited": return true default: return false } } func isDocumentLifecycleEvent(eventType string) bool { return eventType == DocumentReloadEvent || eventType == DocumentLoadCompleteEvent || eventType == DocumentLoadStoppedEvent }