Files

112 lines
3.1 KiB
Go

// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
// SPDX-License-Identifier: GPL-3.0-or-later
package review
import (
"context"
"errors"
"git.stormux.org/storm/cthulhu/internal/atspi"
)
// Interest belongs to the review worker. Zone bounds are provider coordinates;
// callers must resolve fresh source/window geometry before screen capture.
// A flat zone must never silently fall back to the larger object's bounds.
type Interest struct {
Offset int // -1 means use the native caret; virtual browser carets supply an offset.
Object atspi.ObjectID
Zone atspi.Rectangle
HasZone bool
}
type interestMode uint8
const (
implicitInterest interestMode = iota
objectInterest
flatInterest
)
type interestReply struct {
interest Interest
err error
}
type interestRequest struct {
ctx context.Context
result chan interestReply
}
// ObjectOfInterest is ordered with navigation, and never captures a tree. Call
// it on a worker. Explicit review wins over a virtual browser caret, then focus.
// Failed/stale explicit review returns an error instead of capturing focus.
func (worker *Worker) ObjectOfInterest(ctx context.Context) (Interest, error) {
if err := ctx.Err(); err != nil {
return Interest{}, err
}
worker.stateMutex.Lock()
done, started := worker.done, worker.started
worker.stateMutex.Unlock()
if !started {
return Interest{}, errors.New("review worker is not running")
}
select {
case <-done:
return Interest{}, ErrNoCurrent
default:
}
query := &interestRequest{ctx: ctx, result: make(chan interestReply, 1)}
select {
case worker.requests <- request{interest: query}:
case <-ctx.Done():
return Interest{}, ctx.Err()
case <-done:
return Interest{}, ErrNoCurrent
}
select {
case reply := <-query.result:
return reply.interest, reply.err
case <-ctx.Done():
return Interest{}, ctx.Err()
case <-done:
return Interest{}, ErrNoCurrent
}
}
func resolve_interest(ctx context.Context, focus atspi.ObjectID, explicit interestMode, cursor *Cursor, flat *FlatCursor, flatTree *atspi.AccessibleTree, document *DocumentCursor) (Interest, error) {
if err := ctx.Err(); err != nil {
return Interest{}, err
}
if !focus.Valid() {
return Interest{}, ErrNoCurrent
}
switch explicit {
case objectInterest:
if cursor == nil {
return Interest{}, ErrNoCurrent
}
position, err := cursor.Current()
return Interest{Object: position.Current.ID(), Offset: -1}, err
case flatInterest:
if err := flat.Refresh(ctx, flatTree, focus); err != nil {
return Interest{}, err
}
position := flat.position(Character)
if !position.Bounds.Valid() {
return Interest{}, atspi.ErrStaleGeometry
}
return Interest{Object: position.Object, Offset: -1, Zone: position.Bounds, HasZone: true}, nil
}
if document != nil {
position, err := document.SyncFocus(ctx, focus)
if err == nil && position.Origin != NativeNavigation {
return Interest{Object: position.Object.ID(), Offset: position.Offset}, nil
}
if err != nil && !errors.Is(err, atspi.ErrNotInDocument) && !errors.Is(err, atspi.ErrObjectNotFound) {
return Interest{}, err
}
}
return Interest{Object: focus, Offset: -1}, nil
}