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.Document(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{} 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 }