Files
cthulhu/internal/review/document_braille_routing.go

135 lines
4.5 KiB
Go

package review
import (
"context"
"time"
"git.stormux.org/storm/cthulhu/internal/atspi"
)
type documentBrailleRequest struct {
ctx context.Context
focus, document, target atspi.ObjectID
line string
start, offset int
done chan error
}
// RouteDocumentBraille shares the navigation FIFO. The display session may
// wait, but input and event callbacks never wait for native caret placement.
func (worker *Worker) RouteDocumentBraille(ctx context.Context, epoch uint64, focus, document, target atspi.ObjectID, line string, start, offset int) error {
if err := ctx.Err(); err != nil {
return err
}
route := &documentBrailleRequest{ctx: ctx, focus: focus, document: document, target: target, line: line, start: start, offset: offset, done: make(chan error, 1)}
select {
case worker.requests <- request{brailleRoute: route, documentEpoch: epoch, queuedAt: time.Now()}:
default:
return ErrRequestQueueFull
}
select {
case err := <-route.done:
return err
case <-ctx.Done():
return ctx.Err()
}
}
func (worker *Worker) route_document_braille(ctx context.Context, cursor *DocumentCursor, request request) (result Result) {
route := request.brailleRoute
result = Result{DocumentBrailleRoute: true, DocumentCaretCommand: CaretReadLine, DocumentFocus: route.focus, DocumentEpoch: request.documentEpoch}
defer func() { route.done <- result.Err }()
if cursor == nil || worker.focus() != route.focus || !worker.CurrentDocumentResult(result) {
result.Err = atspi.ErrStaleObject
return
}
ctx, cancel := context.WithTimeout(ctx, worker.captureTimeout)
stop := context.AfterFunc(route.ctx, cancel)
defer stop()
defer cancel()
if result.Err = route.ctx.Err(); result.Err != nil {
return
}
target, err := cursor.tree.Reference(route.target)
if err != nil {
result.Err = err
return
}
owner, err := cursor.tree.DocumentContext(target)
if err != nil || owner.TopLevel.ID() != route.document {
result.Err = atspi.ErrStaleObject
return
}
router, ok := worker.nativeCapturerFor(route.focus).(interface {
RouteBraille(context.Context, atspi.BrailleRoute) error
})
if !ok {
result.Err = atspi.ErrNoDocumentCaret
return
}
reader, ok := worker.nativeCapturerFor(route.focus).(DocumentCaretReader)
if !ok {
result.Err = atspi.ErrNoDocumentCaret
return
}
doc, err := cursor.tree.DocumentView(ctx, target)
if err != nil {
result.Err = err
return
}
node, caretOffset, err := native_caret_node(ctx, reader, doc, route.target, route.offset)
if err != nil {
result.Err = err
return
}
if node.Hidden {
result.Err = atspi.ErrStaleObject
return
}
result.Err = router.RouteBraille(ctx, atspi.BrailleRoute{Focus: route.focus, Position: target, Line: route.line, Start: route.start, Offset: route.offset})
if result.Err != nil || worker.focus() != route.focus || !worker.CurrentDocumentResult(result) {
if result.Err == nil {
result.Err = atspi.ErrStaleObject
}
return
}
// Keep the document anchor; a routed inline child is not native focus.
cursor.mu.Lock()
if len(cursor.places) == 0 || cursor.active.ID() != route.document {
cursor.mu.Unlock()
result.Err = atspi.ErrStaleObject
return
}
place := &cursor.places[len(cursor.places)-1]
place.object, place.offset, place.origin = node.Reference, caretOffset, CaretNavigation
place.tableCell = atspi.ObjectID{}
remember_text_parent(doc, place)
snapshot, err := cursor.tree.Resolve(node.Reference)
result.DocumentPosition = DocumentPosition{Document: owner.TopLevel, Object: node.Reference, Snapshot: snapshot, Offset: caretOffset, Origin: CaretNavigation}
cursor.mu.Unlock()
result.Err, result.Moved = err, err == nil
result.DocumentCaretContext = owner.Ancestors
if result.Moved && worker.documentBraille.Load() {
// Optional assembly refresh preserves surrounding inline fragments while
// retaining the exact routed offset. It never repeats caret placement.
deadline, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
defer cancel()
indices := caretNodeRanges(doc.Nodes)
position := -1
for index, candidate := range indices {
if doc.Nodes[candidate.index].Reference == node.Reference && caretOffset >= candidate.start && caretOffset < candidate.end {
position = index
break
}
}
if position >= 0 {
parts, err := collect_visual_line(deadline, reader, doc, indices, visual_line_owners(doc.Nodes), position, caretOffset, true)
if err == nil && len(parts) > 1 {
line, _, _ := assemble_visual_line(cursor.tree, doc, parts)
result.DocumentCaretLine = &line
}
}
}
return
}