Files
cthulhu/internal/atspi/mouse_review_native.go

239 lines
7.2 KiB
Go

// 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
}