11package accessibility
22
33import (
4- "context"
5- "sync"
4+ "image"
5+
6+ "github.com/y3owk1n/govim/internal/logger"
7+ "go.uber.org/zap"
68)
79
810// TreeNode represents a node in the accessibility tree
@@ -15,18 +17,17 @@ type TreeNode struct {
1517
1618// TreeOptions configures tree traversal
1719type TreeOptions struct {
18- MaxDepth int
19- IncludeInvisible bool
20- FilterFunc func (* ElementInfo ) bool
21- MaxConcurrent int
20+ MaxDepth int
21+ FilterFunc func (* ElementInfo ) bool
22+ IncludeOutOfBounds bool
2223}
2324
2425// DefaultTreeOptions returns default tree traversal options
2526func DefaultTreeOptions () TreeOptions {
2627 return TreeOptions {
27- MaxDepth : 10 ,
28- MaxConcurrent : 10 ,
29- FilterFunc : nil ,
28+ MaxDepth : 10 ,
29+ FilterFunc : nil ,
30+ IncludeOutOfBounds : false ,
3031 }
3132}
3233
@@ -41,36 +42,84 @@ func BuildTree(root *Element, opts TreeOptions) (*TreeNode, error) {
4142 return nil , err
4243 }
4344
45+ // Calculate window bounds for spatial filtering
46+ windowBounds := image .Rect (
47+ info .Position .X ,
48+ info .Position .Y ,
49+ info .Position .X + info .Size .X ,
50+ info .Position .Y + info .Size .Y ,
51+ )
52+
53+ // Add padding to catch elements slightly outside
54+ windowBounds = expandRectangle (windowBounds , 50 )
55+
4456 node := & TreeNode {
4557 Element : root ,
4658 Info : info ,
4759 }
4860
4961 if opts .MaxDepth > 0 {
50- buildTreeRecursive (node , 1 , opts )
62+ buildTreeRecursive (node , 1 , opts , windowBounds )
5163 }
5264
5365 return node , nil
5466}
5567
56- func buildTreeRecursive (parent * TreeNode , depth int , opts TreeOptions ) {
68+ func buildTreeRecursive (parent * TreeNode , depth int , opts TreeOptions , windowBounds image. Rectangle ) {
5769 if depth >= opts .MaxDepth {
5870 return
5971 }
60-
6172 children , err := parent .Element .GetChildren ()
6273 if err != nil || len (children ) == 0 {
6374 return
6475 }
6576
66- parent .Children = make ([]* TreeNode , 0 , len (children ))
77+ totalChildren := len (children )
78+
79+ // Smart sampling for large containers
80+ indicesToCheck := getIndicesToCheck (totalChildren , parent .Info .Role )
81+
82+ // Print sampling stats for large containers
83+ if totalChildren > 50 {
84+ logger .Debug ("Sampling" ,
85+ zap .String ("role" , parent .Info .Role ),
86+ zap .Int ("total_children" , totalChildren ),
87+ zap .Int ("indices_to_check" , len (indicesToCheck )),
88+ zap .Float64 ("percent" , float64 (len (indicesToCheck ))/ float64 (totalChildren )* 100 ),
89+ )
90+ }
6791
68- for _ , child := range children {
92+ parent .Children = make ([]* TreeNode , 0 , len (indicesToCheck ))
93+
94+ checkedCount := 0
95+ addedCount := 0
96+
97+ for _ , idx := range indicesToCheck {
98+ if idx >= len (children ) {
99+ continue
100+ }
101+ child := children [idx ]
69102 info , err := child .GetInfo ()
70103 if err != nil {
71104 continue
72105 }
73106
107+ checkedCount ++
108+
109+ // Skip elements that are completely outside the window bounds
110+ // UNLESS IncludeOutOfBounds is true
111+ if ! opts .IncludeOutOfBounds {
112+ elementRect := image .Rect (
113+ info .Position .X ,
114+ info .Position .Y ,
115+ info .Position .X + info .Size .X ,
116+ info .Position .Y + info .Size .Y ,
117+ )
118+ if ! elementRect .Overlaps (windowBounds ) {
119+ continue
120+ }
121+ }
122+
74123 // Apply filter if provided
75124 if opts .FilterFunc != nil && ! opts .FilterFunc (info ) {
76125 continue
@@ -82,90 +131,85 @@ func buildTreeRecursive(parent *TreeNode, depth int, opts TreeOptions) {
82131 Parent : parent ,
83132 Children : []* TreeNode {},
84133 }
85-
86134 parent .Children = append (parent .Children , childNode )
87- buildTreeRecursive (childNode , depth + 1 , opts )
135+ addedCount ++
136+ buildTreeRecursive (childNode , depth + 1 , opts , windowBounds )
88137 }
89- }
90138
91- // BuildTreeConcurrent builds an accessibility tree using concurrent traversal
92- func BuildTreeConcurrent (root * Element , opts TreeOptions ) (* TreeNode , error ) {
93- if root == nil {
94- return nil , nil
139+ // Summary for this container
140+ if totalChildren > 50 {
141+ logger .Debug ("Result: " ,
142+ zap .Int ("checked_count" , checkedCount ),
143+ zap .Int ("added_count" , addedCount ),
144+ zap .Int ("skipped_count" , totalChildren - checkedCount ),
145+ )
95146 }
147+ }
96148
97- info , err := root .GetInfo ()
98- if err != nil {
99- return nil , err
149+ // getIndicesToCheck returns which indices to check based on container type and size
150+ func getIndicesToCheck (totalChildren int , role string ) []int {
151+ // For non-list containers, check all children
152+ if role != "AXList" && role != "AXTable" && role != "AXOutline" {
153+ indices := make ([]int , totalChildren )
154+ for i := range indices {
155+ indices [i ] = i
156+ }
157+ return indices
100158 }
101159
102- node := & TreeNode {
103- Element : root ,
104- Info : info ,
160+ // For small lists, check everything
161+ if totalChildren <= 50 {
162+ indices := make ([]int , totalChildren )
163+ for i := range indices {
164+ indices [i ] = i
165+ }
166+ return indices
105167 }
106168
107- if opts .MaxDepth > 0 {
108- ctx := context .Background ()
109- sem := make (chan struct {}, opts .MaxConcurrent )
110- var wg sync.WaitGroup
169+ // For very large lists (>1000), be MUCH more conservative
170+ if totalChildren > 1000 {
171+ indices := make ([]int , 0 , 50 )
111172
112- buildTreeConcurrentRecursive (ctx , node , 1 , opts , sem , & wg )
113- wg .Wait ()
114- }
173+ // First 20 items
174+ for i := 0 ; i < 20 && i < totalChildren ; i ++ {
175+ indices = append (indices , i )
176+ }
115177
116- return node , nil
117- }
178+ // Sample every 100th item in the middle (or every 5% of total, whichever is larger)
179+ step := max (100 , totalChildren / 20 )
180+ for i := 20 ; i < totalChildren - 20 ; i += step {
181+ indices = append (indices , i )
182+ }
118183
119- func buildTreeConcurrentRecursive (ctx context.Context , parent * TreeNode , depth int , opts TreeOptions , sem chan struct {}, wg * sync.WaitGroup ) {
120- if depth >= opts .MaxDepth {
121- return
122- }
184+ // Last 20 items
185+ start := max (totalChildren - 20 , 20 )
186+ for i := start ; i < totalChildren ; i ++ {
187+ indices = append (indices , i )
188+ }
123189
124- children , err := parent .Element .GetChildren ()
125- if err != nil || len (children ) == 0 {
126- return
190+ return indices
127191 }
128192
129- parent .Children = make ([]* TreeNode , 0 , len (children ))
130- var mu sync.Mutex
131-
132- for _ , child := range children {
133- wg .Add (1 )
134- go func (child * Element ) {
135- defer wg .Done ()
136-
137- // Acquire semaphore
138- select {
139- case sem <- struct {}{}:
140- defer func () { <- sem }()
141- case <- ctx .Done ():
142- return
143- }
144-
145- info , err := child .GetInfo ()
146- if err != nil {
147- return
148- }
149-
150- // Apply filter if provided
151- if opts .FilterFunc != nil && ! opts .FilterFunc (info ) {
152- return
153- }
193+ // For medium lists (50-1000), use the original strategy
194+ indices := make ([]int , 0 , 100 )
154195
155- childNode := & TreeNode {
156- Element : child ,
157- Info : info ,
158- Parent : parent ,
159- Children : []* TreeNode {},
160- }
196+ // First 30
197+ for i := 0 ; i < 30 && i < totalChildren ; i ++ {
198+ indices = append (indices , i )
199+ }
161200
162- mu .Lock ()
163- parent .Children = append (parent .Children , childNode )
164- mu .Unlock ()
201+ // Sample middle (every 10th)
202+ for i := 30 ; i < totalChildren - 20 ; i += 10 {
203+ indices = append (indices , i )
204+ }
165205
166- buildTreeConcurrentRecursive (ctx , childNode , depth + 1 , opts , sem , wg )
167- }(child )
206+ // Last 20
207+ start := max (totalChildren - 20 , 30 )
208+ for i := start ; i < totalChildren ; i ++ {
209+ indices = append (indices , i )
168210 }
211+
212+ return indices
169213}
170214
171215// FindClickableElements finds all clickable elements in the tree
0 commit comments