Cthulhu is no longer a fork. Now it is a screen reader written in Golang.
This commit is contained in:
1 parent
a998fe67de
commit
069fc674dc
708 files changed
+112482
-532
No files matched your search
@@ -0,0 +1,121 @@
|
||||
// Copyright (c) 2026 Storm Dragon
|
||||
// Authors: Storm Dragon, Angus Kola
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package review
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.stormux.org/storm/cthulhu/internal/atspi"
|
||||
)
|
||||
|
||||
// Native selections can report their caret on a label/paragraph rather than
|
||||
// its static text child. Transfer offsets only when both expose the identical
|
||||
// text range; embedded or ambiguous child offsets must never be guessed.
|
||||
func (cursor *DocumentCursor) syncNativeCaret(ctx context.Context, reader DocumentCaretReader, focus atspi.ObjectID, offset int) error {
|
||||
if offset < 0 {
|
||||
return atspi.ErrNoDocumentCaret
|
||||
}
|
||||
if _, err := cursor.SyncFocus(ctx, focus); err != nil {
|
||||
return err
|
||||
}
|
||||
cursor.mu.Lock()
|
||||
defer cursor.mu.Unlock()
|
||||
place := &cursor.places[len(cursor.places)-1]
|
||||
doc, err := cursor.tree.Document(ctx, place.focus)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
node, err := native_caret_node(ctx, reader, doc, focus, offset)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
place.object, place.offset, place.origin = node.Reference, offset, CaretNavigation
|
||||
place.tableCell = atspi.ObjectID{}
|
||||
return nil
|
||||
}
|
||||
|
||||
func native_caret_node(ctx context.Context, reader DocumentCaretReader, doc atspi.DocumentSnapshot, focus atspi.ObjectID, offset int) (atspi.DocumentNode, error) {
|
||||
nodes := make(map[atspi.ObjectID]atspi.DocumentNode, len(doc.Nodes))
|
||||
for _, node := range doc.Nodes {
|
||||
nodes[node.Reference.ID()] = node
|
||||
}
|
||||
node := nodes[focus]
|
||||
if !node.Reference.Valid() || offset < 0 || offset > node.Snapshot.CharacterCount || node.Snapshot.Editable || node.Snapshot.Role == "password text" {
|
||||
return atspi.DocumentNode{}, atspi.ErrNoDocumentCaret
|
||||
}
|
||||
for len(node.Children) != 0 && !wholeCaretNode(node.Snapshot) {
|
||||
if len(node.Children) != 1 {
|
||||
return atspi.DocumentNode{}, atspi.ErrNoDocumentCaret
|
||||
}
|
||||
child, ok := nodes[node.Children[0]]
|
||||
if !ok || child.Hidden || child.Snapshot.Editable || child.Snapshot.CharacterCount != node.Snapshot.CharacterCount {
|
||||
return atspi.DocumentNode{}, atspi.ErrNoDocumentCaret
|
||||
}
|
||||
probeOffset := max(0, min(offset, node.Snapshot.CharacterCount-1))
|
||||
parentText, err := reader.ReadDocumentCaret(ctx, node.Reference, probeOffset, atspi.DocumentCaretBrailleLine)
|
||||
if err != nil {
|
||||
return atspi.DocumentNode{}, err
|
||||
}
|
||||
childText, err := reader.ReadDocumentCaret(ctx, child.Reference, probeOffset, atspi.DocumentCaretBrailleLine)
|
||||
if err != nil {
|
||||
return atspi.DocumentNode{}, err
|
||||
}
|
||||
if parentText.Start != childText.Start || parentText.End != childText.End || parentText.Text == "" || parentText.Text != childText.Text {
|
||||
return atspi.DocumentNode{}, atspi.ErrNoDocumentCaret
|
||||
}
|
||||
node = child
|
||||
}
|
||||
return node, nil
|
||||
}
|
||||
|
||||
// Use the navigation FIFO so a subsequent intercepted arrow sees the native
|
||||
// selection endpoint, even if capturing the document is still in progress.
|
||||
func (worker *Worker) SyncDocumentCaret(focus atspi.ObjectID, offset int) error {
|
||||
if !focus.Valid() || offset < 0 {
|
||||
return atspi.ErrNoDocumentCaret
|
||||
}
|
||||
select {
|
||||
case worker.requests <- request{documentEpoch: worker.documentEpoch.Load(), nativeCaret: true, nativeCaretFocus: focus, nativeCaretOffset: offset}:
|
||||
return nil
|
||||
default:
|
||||
return ErrRequestQueueFull
|
||||
}
|
||||
}
|
||||
|
||||
func (worker *Worker) syncDocumentCaret(ctx context.Context, cursor *DocumentCursor, request request) (Result, *DocumentCursor) {
|
||||
result := Result{DocumentNativeCaret: true, DocumentEpoch: request.documentEpoch, DocumentFocus: request.nativeCaretFocus}
|
||||
if worker.focus() != request.nativeCaretFocus {
|
||||
result.Err = atspi.ErrStaleObject
|
||||
return result, cursor
|
||||
}
|
||||
reader, ok := worker.nativeCapturerFor(request.nativeCaretFocus).(DocumentCaretReader)
|
||||
if !ok {
|
||||
result.Err = atspi.ErrNoDocumentCaret
|
||||
return result, cursor
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(ctx, worker.captureTimeout)
|
||||
defer cancel()
|
||||
if cursor == nil {
|
||||
cursor, result.Err = worker.refreshDocumentCursor(ctx, nil, request.nativeCaretFocus)
|
||||
if result.Err != nil {
|
||||
return result, cursor
|
||||
}
|
||||
}
|
||||
result.Err = cursor.syncNativeCaret(ctx, reader, request.nativeCaretFocus, request.nativeCaretOffset)
|
||||
if documentNeedsCapture(result.Err) {
|
||||
fresh, captureErr := worker.refreshDocumentCursor(ctx, cursor, request.nativeCaretFocus)
|
||||
if captureErr == nil && worker.focus() == request.nativeCaretFocus {
|
||||
cursor = fresh
|
||||
result.Err = cursor.syncNativeCaret(ctx, reader, request.nativeCaretFocus, request.nativeCaretOffset)
|
||||
} else if captureErr != nil {
|
||||
result.Err = captureErr
|
||||
}
|
||||
}
|
||||
if result.Err == nil {
|
||||
result.DocumentPosition, result.Err = cursor.SyncFocus(ctx, request.nativeCaretFocus)
|
||||
}
|
||||
return result, cursor
|
||||
}
|
||||
Reference in new issue
Block a user