1package atspi
2
3import "github.com/bnema/neferwl/internal/ports"
4
5// AT-SPI constants (atspi-constants.h). The role ids are the AtspiRole enum
6// values; the state ids are bit indices of the state set.
7const (
8 roleCheckBox = 7
9 roleComboBox = 11
10 roleDialog = 16
11 roleFrame = 23
12 roleMenuItem = 35
13 rolePageTab = 37
14 rolePushButton = 43
15 roleRadioButton = 44
16 roleToggleButton = 62
17 roleWindow = 69
18 roleLink = 88
19 roleDocumentFrame = 82
20 roleDocumentWeb = 95
21
22 stateActive = 1
23 stateShowing = 25
24 stateVisible = 30
25
26 roleWords = 5 // int32 words of a role set in a MatchRule
27 stateWords = 2 // int32 words of a state set in a MatchRule
28
29 // AtspiCollectionMatchType and AtspiCollectionSortOrder values.
30 matchAll = 1
31 matchAny = 2
32 sortCanonical = 1
33 matchCount = 500
34 maxDepth = 30
35 maxWalkNodes = 3000
36 maxFrames = 32
37 sizeTolerance = 40
38 maxMatchesRead = matchCount
39)
40
41// clickableRoles are the roles that become hint targets.
42var clickableRoles = []uint32{
43 roleLink, rolePushButton, roleToggleButton, roleCheckBox,
44 roleRadioButton, roleMenuItem, rolePageTab, roleComboBox,
45}
46
47// matchRule is the Collection query: showing and visible objects of the
48// clickable roles (only links when linksOnly), plus web documents for coverage.
49type matchRule struct{ linksOnly bool }
50
51func (r matchRule) roles() []uint32 {
52 if r.linksOnly {
53 return []uint32{roleLink, roleDocumentFrame, roleDocumentWeb}
54 }
55 return append(append([]uint32(nil), clickableRoles...), roleDocumentFrame, roleDocumentWeb)
56}
57
58// roleBits packs role ids into the bit set of a MatchRule: bit id%32 of word
59// id/32.
60func roleBits(ids []uint32) []int32 {
61 words := make([]int32, roleWords)
62 for _, id := range ids {
63 if w := int(id / 32); w < len(words) {
64 words[w] |= int32(uint32(1) << (id % 32))
65 }
66 }
67 return words
68}
69
70// stateBits packs state ids into the bit set of a MatchRule.
71func stateBits(ids ...uint32) []int32 {
72 words := make([]int32, stateWords)
73 for _, id := range ids {
74 if w := int(id / 32); w < len(words) {
75 words[w] |= int32(uint32(1) << (id % 32))
76 }
77 }
78 return words
79}
80
81func hasState(set uint64, state uint) bool { return set&(1<<state) != 0 }
82
83// isClickable reports whether role makes a target.
84func isClickable(role uint32, linksOnly bool) bool {
85 if linksOnly {
86 return role == roleLink
87 }
88 for _, r := range clickableRoles {
89 if r == role {
90 return true
91 }
92 }
93 return false
94}
95
96// isWindowRole reports whether role is a top-level window role.
97func isWindowRole(role uint32) bool {
98 return role == roleFrame || role == roleWindow || role == roleDialog
99}
100
101// kindOf maps a role to the hint kind: links are links, the rest buttons.
102func kindOf(role uint32) ports.HintKind {
103 if role == roleLink {
104 return ports.HintLink
105 }
106 return ports.HintButton
107}
108
109// frameInfo is a showing top-level window of an app.
110type frameInfo struct {
111 ref ref
112 active bool
113 w, h int // size of the frame, from its window extents
114}
115
116// matchFrames pairs the windows of one app with its frames, one to one. A
117// window is paired with a frame when the pairing is certain:
118// - one window and one frame;
119// - the focused window with the one ACTIVE frame; that frame is then
120// reserved and no other window can take it;
121// - otherwise the one unreserved frame whose size is within sizeTolerance
122// of the window, provided no other unmatched window claims it as well.
123//
124// Ambiguous windows are left out. The result maps the index of a window in
125// wins to the index of its frame in frames.
126func matchFrames(wins []ports.HintWindow, frames []frameInfo) map[int]int {
127 out := map[int]int{}
128 if len(wins) == 1 && len(frames) == 1 {
129 out[0] = 0
130 return out
131 }
132 // Active matches first: they are reserved against the size matches.
133 active, n := -1, 0
134 for j, f := range frames {
135 if f.active {
136 active, n = j, n+1
137 }
138 }
139 reserved := make([]bool, len(frames))
140 done := make([]bool, len(wins))
141 if n == 1 {
142 focused := -1
143 count := 0
144 for i, w := range wins {
145 if w.Focused {
146 focused, count = i, count+1
147 }
148 }
149 if count == 1 {
150 out[focused] = active
151 reserved[active] = true
152 done[focused] = true
153 }
154 }
155 claims := make([][]int, len(wins)) // size candidates per window
156 owners := make([]int, len(frames)) // windows that size-match a frame
157 for i, w := range wins {
158 if done[i] {
159 continue
160 }
161 for j, f := range frames {
162 if !reserved[j] && absInt(f.w-w.Rect.W) <= sizeTolerance && absInt(f.h-w.Rect.H) <= sizeTolerance {
163 claims[i] = append(claims[i], j)
164 owners[j]++
165 }
166 }
167 }
168 for i := range wins {
169 if !done[i] && len(claims[i]) == 1 && owners[claims[i][0]] == 1 {
170 out[i] = claims[i][0]
171 }
172 }
173 return out
174}
175
176func absInt(v int) int {
177 if v < 0 {
178 return -v
179 }
180 return v
181}
182
183// element is a match with its extents.
184type element struct {
185 role uint32
186 x, y, w, h int32 // window-relative
187}
188
189// toTarget converts an element of a window into a logical output-local
190// target: the element extents are relative to the top-level frame, whose own
191// window-relative origin is (fx, fy); the result is clipped to the window.
192// It reports false for an empty result.
193func toTarget(win ports.Rect, fx, fy int32, e element) (ports.HintTarget, bool) {
194 if e.w <= 0 || e.h <= 0 {
195 return ports.HintTarget{}, false
196 }
197 r := ports.Rect{X: win.X + int(e.x-fx), Y: win.Y + int(e.y-fy), W: int(e.w), H: int(e.h)}
198 x0, y0 := max(r.X, win.X), max(r.Y, win.Y)
199 x1, y1 := min(r.X+r.W, win.X+win.W), min(r.Y+r.H, win.Y+win.H)
200 if x1 <= x0 || y1 <= y0 {
201 return ports.HintTarget{}, false
202 }
203 return ports.HintTarget{Rect: ports.Rect{X: x0, Y: y0, W: x1 - x0, H: y1 - y0}, Kind: kindOf(e.role)}, true
204}
205
206func isDocument(role uint32) bool { return role == roleDocumentFrame || role == roleDocumentWeb }