112 lines
3.1 KiB
Go
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
|
|
}
|