// 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" "fmt" ) // native_review_table_children enumerates a selectable table's viewport, as // Orca's flat review does. GTK exposes every offscreen row as a child; fetching // that list creates thousands of transient cell proxies during timeline refresh. // Returned references belong to the caller. All queries run on the GLib owner. func native_review_table_children(ctx context.Context, source *C.AtspiAccessible, clip Rectangle) (children []*C.AtspiAccessible, handled bool, err error) { if !clip.Valid() { return nil, false, nil } table := C.atspi_accessible_get_table_iface(source) if table == nil { return nil, false, nil } defer C.g_object_unref(C.gpointer(table)) selection := C.atspi_accessible_get_selection_iface(source) if selection == nil { return nil, false, nil } defer C.g_object_unref(C.gpointer(selection)) component := C.atspi_accessible_get_component_iface(source) if component == nil { return nil, false, nil } defer C.g_object_unref(C.gpointer(component)) defer func() { if err != nil { for _, child := range children { C.g_object_unref(C.gpointer(child)) } children = nil } }() var queryError *C.GError rows := int(C.atspi_table_get_n_rows(table, &queryError)) if queryError != nil { err = nativeError("query review table rows", queryError) C.g_error_free(queryError) return nil, true, err } columns := int(C.atspi_table_get_n_columns(table, &queryError)) if queryError != nil { err = nativeError("query review table columns", queryError) C.g_error_free(queryError) return nil, true, err } if rows < 0 || columns < 0 { return nil, true, fmt.Errorf("%w: negative review table dimensions", ErrInvalidTree) } // Native focus normalization uses the first selected cell of a uniquely // selected row. Keep that identity as an anchor when horizontal scrolling // hides its column; its geometry still excludes it from review output. if rows > 0 { if selected := C.atspi_selection_get_selected_child(selection, 0, nil); selected != nil { children = append(children, selected) } } appendHeaders := func(first, last int) error { if last-first >= maxTreeNodes { return fmt.Errorf("%w: visible table exceeds limit %d", ErrInvalidTree, maxTreeNodes) } for column := first; column <= last; column++ { if err := ctx.Err(); err != nil { return err } header := C.atspi_table_get_column_header(table, C.gint(column), nil) if header == nil { continue } geometry, geometryErr := geometryFromNative(ctx, header) _, intersects := geometry.Bounds.Intersection(clip) if geometryErr == nil && geometry.Visible && geometry.Showing && intersects { children = append(children, header) } else { C.g_object_unref(C.gpointer(header)) } if geometryErr != nil { return geometryErr } } return nil } if rows == 0 || columns == 0 { err = appendHeaders(0, columns-1) return children, true, err } // Start just below the table's header. Hit testing avoids walking from row // zero when the viewport is thousands of rows down the model. A header's // bounds let us skip directly to its bottom edge. seedRow, seedColumn := -1, -1 bottom := int64(clip.Y) + int64(clip.Height) for y := int64(clip.Y) + 1; y < bottom && seedRow < 0; y++ { if err := ctx.Err(); err != nil { return children, true, err } for _, x := range []int64{int64(clip.X) + 1, int64(clip.X) + int64(clip.Width)/2} { child := C.atspi_component_get_accessible_at_point(component, C.gint(x), C.gint(y), C.ATSPI_COORD_TYPE_SCREEN, &queryError) if queryError != nil { err = nativeError("locate visible review table cell", queryError) C.g_error_free(queryError) if child != nil { C.g_object_unref(C.gpointer(child)) } return children, true, err } if child == nil { continue } row, column := -1, -1 if C.atspi_accessible_get_role(child, nil) == C.ATSPI_ROLE_TABLE_CELL { if cell := C.atspi_accessible_get_table_cell(child); cell != nil { var nativeRow, nativeColumn C.gint C.atspi_table_cell_get_position(cell, &nativeRow, &nativeColumn, &queryError) if queryError == nil { row, column = int(nativeRow), int(nativeColumn) } else { err = nativeError("query visible review cell position", queryError) C.g_error_free(queryError) C.g_object_unref(C.gpointer(cell)) C.g_object_unref(C.gpointer(child)) return children, true, err } C.g_object_unref(C.gpointer(cell)) } else { index := C.atspi_accessible_get_index_in_parent(child, nil) row = int(C.atspi_table_get_row_at_index(table, index, nil)) column = int(C.atspi_table_get_column_at_index(table, index, nil)) } } geometry, geometryErr := geometryFromNative(ctx, child) C.g_object_unref(C.gpointer(child)) if geometryErr != nil { return children, true, geometryErr } if row >= 0 && row < rows && column >= 0 && column < columns { seedRow, seedColumn = row, column break } if geometry.BoundsKnown { y = max(y, int64(geometry.Bounds.Y)+int64(geometry.Bounds.Height)-1) } } } if seedRow < 0 { // An empty or clipped body can still have visible column headers. err = appendHeaders(0, columns-1) return children, true, err } visible := func(row, column int, keep bool) (bool, error) { if err := ctx.Err(); err != nil { return false, err } child := C.atspi_table_get_accessible_at(table, C.gint(row), C.gint(column), &queryError) if queryError != nil { err := nativeError("query visible review table cell", queryError) C.g_error_free(queryError) queryError = nil if child != nil { C.g_object_unref(C.gpointer(child)) } return false, err } if child == nil { return false, nil } geometry, geometryErr := geometryFromNative(ctx, child) _, intersects := geometry.Bounds.Intersection(clip) showing := geometry.Visible && geometry.Showing && geometry.BoundsKnown && intersects if keep && showing && geometryErr == nil { children = append(children, child) } else { C.g_object_unref(C.gpointer(child)) } return showing, geometryErr } firstRow, lastRow := seedRow, seedRow firstColumn, lastColumn := seedColumn, seedColumn for _, axis := range []struct { first, last *int limit int row bool }{{&firstRow, &lastRow, rows, true}, {&firstColumn, &lastColumn, columns, false}} { for _, step := range []int{-1, 1} { edge := axis.first if step > 0 { edge = axis.last } for next := *edge + step; next >= 0 && next < axis.limit; next += step { if *axis.last-*axis.first >= maxTreeNodes { return children, true, fmt.Errorf("%w: visible table exceeds limit %d", ErrInvalidTree, maxTreeNodes) } row, column := seedRow, seedColumn if axis.row { row = next } else { column = next } showing, err := visible(row, column, false) if err != nil { return children, true, err } if !showing { break } *edge = next } } } if err := appendHeaders(firstColumn, lastColumn); err != nil { return children, true, err } for row := firstRow; row <= lastRow; row++ { for column := firstColumn; column <= lastColumn; column++ { if len(children) >= maxTreeNodes { return children, true, fmt.Errorf("%w: visible table exceeds limit %d", ErrInvalidTree, maxTreeNodes) } if _, err := visible(row, column, true); err != nil { return children, true, err } } } return children, true, nil }