package atspi import ( "context" "errors" "math" ) var ErrNoDocumentCaret = errors.New("document caret line unavailable") type DocumentContent struct { Reference ObjectReference Snapshot Snapshot Start int End int Text string RawText string // Separator is display spacing before this object, outside its native range. Separator string Bounds Rectangle } type DocumentLine struct { Context []Snapshot Document ObjectReference Contents []DocumentContent } func (tree *AccessibleTree) DocumentLineAtOffset(ctx context.Context, reference ObjectReference, offset int) (DocumentLine, error) { document, err := tree.Document(ctx, reference) if err != nil { return DocumentLine{}, err } caretIndex := -1 if reference.Valid() { for index, node := range document.Nodes { if node.Reference == reference { caretIndex = index break } } } if caretIndex < 0 { for index, node := range document.Nodes { if node.Text.CaretKnown { caretIndex = index break } } } if caretIndex < 0 { return DocumentLine{}, ErrNoDocumentCaret } caretNode := document.Nodes[caretIndex] if offset < 0 { offset = caretNode.Text.CaretOffset } if offset < 0 { return DocumentLine{}, ErrNoDocumentCaret } caretRange, found := textRangeAtOffset(caretNode.Text, offset) if !found || !caretRange.Bounds.Valid() { return DocumentLine{}, ErrNoDocumentCaret } ancestors := document.Context.Ancestors if len(ancestors) > 0 && ancestors[0].ID() == reference.ID() { ancestors = ancestors[1:] } line := DocumentLine{Document: document.Context.TopLevel, Context: ancestors} for _, node := range document.Nodes { if node.Hidden { continue } for _, textRange := range node.Text.Ranges { if !boundsAreOnSameLine(caretRange.Bounds, textRange.Bounds) { continue } if textRange.Text == "" { continue } line.Contents = append(line.Contents, DocumentContent{ Reference: node.Reference, Snapshot: node.Snapshot, Start: textRange.Start, End: textRange.End, Text: textRange.Text, RawText: textRange.Text, Bounds: textRange.Bounds, }) } } if len(line.Contents) == 0 { return DocumentLine{}, ErrNoDocumentCaret } return line, nil } func textRangeAtOffset(text VisibleText, offset int) (TextRange, bool) { for _, textRange := range text.Ranges { if textRange.Start <= offset && offset < textRange.End { return textRange, true } } for _, textRange := range text.Ranges { if textRange.Start <= offset && offset <= textRange.End { return textRange, true } } return TextRange{}, false } func boundsAreOnSameLine(first, second Rectangle) bool { if first == second { return true } if !first.Valid() || !second.Valid() { return false } top := max(first.Y, second.Y) bottom := min(first.Y+first.Height, second.Y+second.Height) if bottom <= top { return false } firstCenter := first.Y + first.Height/2 secondCenter := second.Y + second.Height/2 return math.Abs(float64(firstCenter-secondCenter)) <= 5 }