// 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" "strings" ) func (adapter *Adapter) clearMouseApplication() { if adapter.mouseApplication != nil { C.g_object_unref(C.gpointer(adapter.mouseApplication)) } adapter.mouseApplication = nil adapter.mouseApplicationPID = 0 } func (adapter *Adapter) mouseApplicationForPID(ctx context.Context, pid int) *C.AtspiAccessible { if pid <= 0 || ctx.Err() != nil { return nil } if adapter.mouseApplicationPID == pid && adapter.mouseApplication != nil { return adapter.mouseApplication } adapter.clearMouseApplication() desktop := C.atspi_get_desktop(0) if desktop == nil { return nil } defer C.g_object_unref(C.gpointer(desktop)) count := min(int(C.atspi_accessible_get_child_count(desktop, nil)), 128) for i := 0; i < count && ctx.Err() == nil; i++ { app := C.atspi_accessible_get_child_at_index(desktop, C.gint(i), nil) if app == nil { continue } if int(C.atspi_accessible_get_process_id(app, nil)) == pid { adapter.mouseApplication, adapter.mouseApplicationPID = app, pid return app } C.g_object_unref(C.gpointer(app)) } return nil } func nativeContainsPoint(source *C.AtspiAccessible, x, y int) bool { component := C.atspi_accessible_get_component_iface(source) if component == nil { return false } defer C.g_object_unref(C.gpointer(component)) var nativeErr *C.GError inside := C.atspi_component_contains(component, C.gint(x), C.gint(y), C.ATSPI_COORD_TYPE_SCREEN, &nativeErr) != 0 if nativeErr != nil { C.g_error_free(nativeErr) return false } return inside } func nativePointArea(source *C.AtspiAccessible) int64 { component := C.atspi_accessible_get_component_iface(source) if component == nil { return 1 << 62 } defer C.g_object_unref(C.gpointer(component)) var nativeErr *C.GError rect := C.atspi_component_get_extents(component, C.ATSPI_COORD_TYPE_SCREEN, &nativeErr) if nativeErr != nil { C.g_error_free(nativeErr) } if rect == nil { return 1 << 62 } defer C.g_free(C.gpointer(rect)) if rect.width <= 0 || rect.height <= 0 { return 1 << 62 } return int64(rect.width) * int64(rect.height) } func nativeAccessibleAtPoint(source *C.AtspiAccessible, x, y int) *C.AtspiAccessible { component := C.atspi_accessible_get_component_iface(source) if component == nil { return nil } defer C.g_object_unref(C.gpointer(component)) var nativeErr *C.GError child := C.atspi_component_get_accessible_at_point(component, C.gint(x), C.gint(y), C.ATSPI_COORD_TYPE_SCREEN, &nativeErr) if nativeErr != nil { C.g_error_free(nativeErr) } return child } func nativePointText(ctx context.Context, source, window *C.AtspiAccessible, x, y int) (word, line string, lineStart, nativeOffset int) { if C.atspi_accessible_get_role(source, nil) == C.ATSPI_ROLE_PASSWORD_TEXT { return "", "", -1, -1 } text := C.atspi_accessible_get_text_iface(source) if text == nil { return "", "", -1, -1 } defer C.g_object_unref(C.gpointer(text)) var nativeErr *C.GError offset := C.atspi_text_get_offset_at_point(text, C.gint(x), C.gint(y), C.ATSPI_COORD_TYPE_SCREEN, &nativeErr) if nativeErr != nil { C.g_error_free(nativeErr) nativeErr = nil } if offset < 0 { component := C.atspi_accessible_get_component_iface(window) if component != nil { rect := C.atspi_component_get_extents(component, C.ATSPI_COORD_TYPE_SCREEN, &nativeErr) if nativeErr != nil { C.g_error_free(nativeErr) nativeErr = nil } if rect != nil { offset = C.atspi_text_get_offset_at_point(text, C.gint(x)-rect.x, C.gint(y)-rect.y, C.ATSPI_COORD_TYPE_WINDOW, &nativeErr) C.g_free(C.gpointer(rect)) if nativeErr != nil { C.g_error_free(nativeErr) nativeErr = nil } } C.g_object_unref(C.gpointer(component)) } } if offset < 0 { // GTK text entries can report -1 at points inside displayed glyphs. // Inspect a bounded number of character rectangles as a fallback. count := min(int(C.atspi_text_get_character_count(text, nil)), 128) for i := 0; i < count && ctx.Err() == nil; i++ { rect := C.atspi_text_get_character_extents(text, C.gint(i), C.ATSPI_COORD_TYPE_SCREEN, &nativeErr) if nativeErr != nil { C.g_error_free(nativeErr) nativeErr = nil } if rect == nil { continue } inside := C.gint(x) >= rect.x && C.gint(x) < rect.x+rect.width && C.gint(y) >= rect.y && C.gint(y) < rect.y+rect.height C.g_free(C.gpointer(rect)) if inside { offset = C.gint(i) break } } } if offset < 0 { return "", "", -1, -1 } read := func(granularity C.AtspiTextGranularity) (string, int) { var err *C.GError value := C.atspi_text_get_string_at_offset(text, offset, granularity, &err) if err != nil { C.g_error_free(err) } if value == nil { return "", -1 } defer C.g_free(C.gpointer(value)) defer C.g_free(C.gpointer(value.content)) return sanitizeSnapshotField(C.GoString(value.content)), int(value.start_offset) } word, _ = read(C.ATSPI_TEXT_GRANULARITY_WORD) line, lineStart = read(C.ATSPI_TEXT_GRANULARITY_LINE) return strings.TrimSpace(word), strings.TrimSpace(line), lineStart, int(offset) } // nativeReviewAtPoint runs on the GLib owner. It retains only the current // application; all window and descendant references are released per query. func (adapter *Adapter) nativeReviewAtPoint(ctx context.Context, x, y, pid int) (PointReview, error) { var result PointReview app := adapter.mouseApplicationForPID(ctx, pid) if app == nil || ctx.Err() != nil { return result, ctx.Err() } var window *C.AtspiAccessible bestArea := int64(1 << 62) count := min(int(C.atspi_accessible_get_child_count(app, nil)), 64) for i := 0; i < count && ctx.Err() == nil; i++ { candidate := C.atspi_accessible_get_child_at_index(app, C.gint(i), nil) if candidate == nil { continue } if nativeContainsPoint(candidate, x, y) { area := nativePointArea(candidate) if window == nil || area <= bestArea { if window != nil { C.g_object_unref(C.gpointer(window)) } window, bestArea = candidate, area continue } } C.g_object_unref(C.gpointer(candidate)) } if window == nil || ctx.Err() != nil { if window != nil { C.g_object_unref(C.gpointer(window)) } return result, ctx.Err() } defer C.g_object_unref(C.gpointer(window)) result.Window = nativeObjectID(window) current := (*C.AtspiAccessible)(C.g_object_ref(C.gpointer(window))) defer func() { C.g_object_unref(C.gpointer(current)) }() for depth := 0; depth < 24 && ctx.Err() == nil; depth++ { child := nativeAccessibleAtPoint(current, x, y) if child == nil { break } if nativeObjectID(child) == nativeObjectID(current) || !nativeContainsPoint(child, x, y) { C.g_object_unref(C.gpointer(child)) break } C.g_object_unref(C.gpointer(current)) current = child } if ctx.Err() != nil { return PointReview{}, ctx.Err() } result.Snapshot = snapshotFromNativeWithOptions(current, false, false, false, false, true) if result.Snapshot.Defunct || result.Snapshot.Role == "password text" { return result, nil } result.Word, result.Line, result.LineStart, result.Offset = nativePointText(ctx, current, window, x, y) return result, nil }