Files

168 lines
5.4 KiB
Go

// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
// SPDX-License-Identifier: GPL-3.0-or-later
package review
import (
"context"
"fmt"
"strings"
"git.stormux.org/storm/cthulhu/internal/atspi"
)
// moveTable uses provider coordinates, never child order as a rectangular grid.
// The virtual coordinate survives a spanning cell so reversing direction keeps
// the user's column/row. Nested tables have independent coordinate spaces.
func (cursor *DocumentCursor) moveTable(ctx context.Context, doc atspi.DocumentSnapshot, place *documentPlace, command DocumentCaretCommand) (DocumentCaretMove, error) {
nodes := make(map[atspi.ObjectID]atspi.DocumentNode, len(doc.Nodes))
parents := make(map[atspi.ObjectID]atspi.ObjectID, len(doc.Nodes))
for _, node := range doc.Nodes {
nodes[node.Reference.ID()] = node
for _, child := range node.Children {
parents[child] = node.Reference.ID()
}
}
nearestTable := func(id atspi.ObjectID) atspi.ObjectID {
for depth := 0; id.Valid() && depth < 128; depth++ {
if nodes[id].Snapshot.Role == "table" || nodes[id].Snapshot.Role == "tree table" {
return id
}
id = parents[id]
}
return atspi.ObjectID{}
}
table := nearestTable(place.object.ID())
message := func(text string) (DocumentCaretMove, error) {
return DocumentCaretMove{Position: documentPosition(doc, *place), Text: text}, nil
}
if !table.Valid() {
return message("Not in a table.")
}
var current atspi.DocumentNode
for id, depth := place.object.ID(), 0; id.Valid() && id != table && depth < 128; id, depth = parents[id], depth+1 {
role := nodes[id].Snapshot.Role
if (role == "table cell" || role == "row header" || role == "column header") && !nodes[id].Snapshot.TablePositionKnown {
return message("Table coordinates unavailable.")
}
if nodes[id].Snapshot.TablePositionKnown {
current = nodes[id]
break
}
}
rows, columns := nodes[table].Snapshot.TableRowCount, nodes[table].Snapshot.TableColumnCount
if rows <= 0 || columns <= 0 {
return message("Table coordinates unavailable.")
}
row, col := current.Snapshot.TableRowIndex, current.Snapshot.TableColumnIndex
if current.Reference.Valid() && place.tableCell == current.Reference.ID() {
row, col = place.tableRow, place.tableColumn
}
dr, dc := 0, 0
switch command {
case TableLeft:
dc = -1
case TableRight:
dc = 1
case TableUp:
dr = -1
case TableDown:
dr = 1
case TableFirst:
row, col = 0, 0
case TableLast:
row, col = rows-1, columns-1
}
// A table structural stop starts at its first cell on directional movement.
if current.Reference.Valid() {
row += dr
col += dc
} else if command != TableLast {
row, col = 0, 0
}
cells := make([]atspi.DocumentNode, 0)
for _, node := range doc.Nodes {
if node.Snapshot.TablePositionKnown && !node.Hidden && nearestTable(node.Reference.ID()) == table {
cells = append(cells, node)
}
}
for attempts := 0; attempts <= len(cells)+1; attempts++ {
if err := ctx.Err(); err != nil {
return DocumentCaretMove{}, err
}
if col < 0 {
return message("Beginning of row.")
}
if col >= columns {
return message("End of row.")
}
if row < 0 {
return message("Top of column.")
}
if row >= rows {
return message("Bottom of column.")
}
var target atspi.DocumentNode
for _, node := range cells {
s := node.Snapshot
if row >= s.TableRowIndex && row < s.TableRowIndex+max(1, s.TableRowSpan) && col >= s.TableColumnIndex && col < s.TableColumnIndex+max(1, s.TableColumnSpan) {
target = node
break
}
}
if !target.Reference.Valid() {
return message("Table cell unavailable.")
}
s := target.Snapshot
if target.Reference == current.Reference && (dr != 0 || dc != 0) {
// Jump over the span rather than iterating potentially huge dimensions.
if dr > 0 {
row = s.TableRowIndex + max(1, s.TableRowSpan)
}
if dr < 0 {
row = s.TableRowIndex - 1
}
if dc > 0 {
col = s.TableColumnIndex + max(1, s.TableColumnSpan)
}
if dc < 0 {
col = s.TableColumnIndex - 1
}
continue
}
parts := []string{}
rowHeader, columnHeader := "", ""
if s.TableRowHeaders != "" && s.TableRowHeaders != current.Snapshot.TableRowHeaders {
rowHeader = s.TableRowHeaders
parts = append(parts, rowHeader)
}
if s.TableColumnHeaders != "" && s.TableColumnHeaders != current.Snapshot.TableColumnHeaders {
columnHeader = s.TableColumnHeaders
parts = append(parts, columnHeader)
}
label := strings.TrimSpace(s.Name)
prefix := strings.Join(parts, " ")
prefixCount := len(parts)
if label == "" {
label = strings.TrimSpace(strings.ReplaceAll(s.Text, "\ufffc", ""))
}
if label == "" {
label = "blank"
}
parts = append(parts, label, fmt.Sprintf("Row %d, column %d.", s.TableRowIndex+1, s.TableColumnIndex+1))
if s.TableRowSpan > 1 {
parts = append(parts, fmt.Sprintf("Spans %d rows.", s.TableRowSpan))
}
if s.TableColumnSpan > 1 {
parts = append(parts, fmt.Sprintf("Spans %d columns.", s.TableColumnSpan))
}
suffix := strings.Join(parts[prefixCount+1:], " ")
place.object, place.offset, place.origin = target.Reference, 0, StructuralNavigation
place.tableCell, place.tableRow, place.tableColumn = target.Reference.ID(), row, col
return DocumentCaretMove{Position: documentPosition(doc, *place), Text: strings.Join(parts, " "), TablePrefix: prefix, TableSuffix: suffix,
TableRowHeader: rowHeader, TableColumnHeader: columnHeader, Moved: true}, nil
}
return message("Table cell unavailable.")
}