Skip to content

Commit 1d4146f

Browse files
authored
feat: add occlusion detection with Dock exception (#34)
1 parent e2055af commit 1d4146f

5 files changed

Lines changed: 223 additions & 22 deletions

File tree

internal/accessibility/element.go

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -212,21 +212,34 @@ func (e *Element) GetInfo() (*ElementInfo, error) {
212212
return info, nil
213213
}
214214

215-
// GetChildren returns all child elements
215+
// GetChildren returns all child elements with optional occlusion checking
216216
func (e *Element) GetChildren() ([]*Element, error) {
217217
if e.ref == nil {
218218
return nil, fmt.Errorf("element is nil")
219219
}
220220

221-
return e.getChildrenInternal(true)
221+
return e.getChildrenInternal(true, false) // visible only, but no occlusion check by default
222222
}
223223

224-
func (e *Element) getChildrenInternal(visibleOnly bool) ([]*Element, error) {
224+
// GetChildrenWithOcclusionCheck returns child elements with strict occlusion filtering
225+
func (e *Element) GetChildrenWithOcclusionCheck(visibleOnly, checkOcclusion bool) ([]*Element, error) {
226+
if e.ref == nil {
227+
return nil, fmt.Errorf("element is nil")
228+
}
229+
230+
return e.getChildrenInternal(visibleOnly, checkOcclusion)
231+
}
232+
233+
func (e *Element) getChildrenInternal(visibleOnly bool, checkOcclusion bool) ([]*Element, error) {
225234
var count C.int
226235
var rawChildren unsafe.Pointer
227236

228237
if visibleOnly {
229-
rawChildren = unsafe.Pointer(C.getVisibleChildren(e.ref, &count))
238+
occlusionFlag := C.int(0)
239+
if checkOcclusion {
240+
occlusionFlag = C.int(1)
241+
}
242+
rawChildren = unsafe.Pointer(C.getVisibleChildren(e.ref, &count, occlusionFlag))
230243
} else {
231244
rawChildren = unsafe.Pointer(C.getChildren(e.ref, &count))
232245
}

internal/accessibility/query.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ func GetDockClickableElements() ([]*TreeNode, error) {
144144

145145
opts := DefaultTreeOptions()
146146
opts.IncludeOutOfBounds = true
147+
opts.CheckOcclusion = false
147148
opts.MaxDepth = 8
148149
opts.FilterFunc = func(info *ElementInfo) bool {
149150
if info.Size.X < 6 || info.Size.Y < 6 {

internal/accessibility/tree.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ type TreeOptions struct {
2020
MaxDepth int
2121
FilterFunc func(*ElementInfo) bool
2222
IncludeOutOfBounds bool
23+
CheckOcclusion bool
2324
}
2425

2526
// DefaultTreeOptions returns default tree traversal options
@@ -28,6 +29,7 @@ func DefaultTreeOptions() TreeOptions {
2829
MaxDepth: 10,
2930
FilterFunc: nil,
3031
IncludeOutOfBounds: false,
32+
CheckOcclusion: false,
3133
}
3234
}
3335

@@ -64,7 +66,7 @@ func buildTreeRecursive(parent *TreeNode, depth int, opts TreeOptions, windowBou
6466
if depth >= opts.MaxDepth {
6567
return
6668
}
67-
children, err := parent.Element.GetChildren()
69+
children, err := parent.Element.GetChildrenWithOcclusionCheck(true, opts.CheckOcclusion)
6870
if err != nil || len(children) == 0 {
6971
return
7072
}

internal/bridge/accessibility.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ void freeElementInfo(ElementInfo* info);
2828
void* getElementAtPosition(CGPoint position);
2929
int getChildrenCount(void* element);
3030
void** getChildren(void* element, int* count);
31-
void** getVisibleChildren(void* element, int* count);
31+
void** getVisibleChildren(void* element, int* count, int checkOcclusion);
3232
int performClick(void* element);
3333
int performRightClick(void* element);
3434
int performDoubleClick(void* element);

internal/bridge/accessibility.m

Lines changed: 201 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -35,34 +35,219 @@ int setApplicationAttribute(int pid, const char* attribute, int value) {
3535
return (error == kAXErrorSuccess) ? 1 : 0;
3636
}
3737

38-
void** getVisibleChildren(void* element, int* count) {
38+
// Helper function to check if a point is actually visible (not occluded by other windows)
39+
static bool isPointVisible(CGPoint point, pid_t elementPid) {
40+
// Use AXUIElementCopyElementAtPosition to check what's at this point
41+
AXUIElementRef systemWide = AXUIElementCreateSystemWide();
42+
if (!systemWide) return true;
43+
44+
AXUIElementRef elementAtPoint = NULL;
45+
AXError error = AXUIElementCopyElementAtPosition(systemWide, point.x, point.y, &elementAtPoint);
46+
CFRelease(systemWide);
47+
48+
if (error != kAXErrorSuccess || !elementAtPoint) {
49+
return true; // Assume visible if we can't check
50+
}
51+
52+
// Get the PID of the element at this point
53+
pid_t pidAtPoint;
54+
bool isVisible = false;
55+
56+
if (AXUIElementGetPid(elementAtPoint, &pidAtPoint) == kAXErrorSuccess) {
57+
// The point is visible if it belongs to the same application
58+
isVisible = (pidAtPoint == elementPid);
59+
}
60+
61+
CFRelease(elementAtPoint);
62+
return isVisible;
63+
}
64+
65+
// Helper function to check if an element is occluded by checking multiple sample points
66+
static bool isElementOccluded(CGRect elementRect, pid_t elementPid) {
67+
// Sample 5 points: center and 4 corners (slightly inset)
68+
CGFloat inset = 2.0; // Inset from edges to avoid border issues
69+
70+
CGPoint samplePoints[5] = {
71+
// Center
72+
CGPointMake(elementRect.origin.x + elementRect.size.width / 2,
73+
elementRect.origin.y + elementRect.size.height / 2),
74+
// Top-left
75+
CGPointMake(elementRect.origin.x + inset,
76+
elementRect.origin.y + inset),
77+
// Top-right
78+
CGPointMake(elementRect.origin.x + elementRect.size.width - inset,
79+
elementRect.origin.y + inset),
80+
// Bottom-left
81+
CGPointMake(elementRect.origin.x + inset,
82+
elementRect.origin.y + elementRect.size.height - inset),
83+
// Bottom-right
84+
CGPointMake(elementRect.origin.x + elementRect.size.width - inset,
85+
elementRect.origin.y + elementRect.size.height - inset)
86+
};
87+
88+
// Element is considered visible if at least 2 sample points are visible
89+
int visiblePoints = 0;
90+
for (int i = 0; i < 5; i++) {
91+
if (isPointVisible(samplePoints[i], elementPid)) {
92+
visiblePoints++;
93+
if (visiblePoints >= 2) {
94+
return false; // Not occluded
95+
}
96+
}
97+
}
98+
99+
return true; // Occluded (less than 2 points visible)
100+
}
101+
102+
// Get children that are actually visible on screen
103+
// checkOcclusion: if true, filters out elements covered by other windows
104+
void** getVisibleChildren(void* element, int* count, int checkOcclusion) {
39105
if (!element || !count) return NULL;
40106

41107
AXUIElementRef axElement = (AXUIElementRef)element;
42-
CFTypeRef visibleChildrenValue = NULL;
43108

44-
if (AXUIElementCopyAttributeValue(axElement, kAXVisibleChildrenAttribute, &visibleChildrenValue) == kAXErrorSuccess && visibleChildrenValue) {
45-
if (CFGetTypeID(visibleChildrenValue) == CFArrayGetTypeID()) {
46-
CFArrayRef children = (CFArrayRef)visibleChildrenValue;
47-
CFIndex childCount = CFArrayGetCount(children);
48-
*count = (int)childCount;
109+
// Get the PID of the element's application
110+
pid_t elementPid;
111+
if (AXUIElementGetPid(axElement, &elementPid) != kAXErrorSuccess) {
112+
*count = 0;
113+
return NULL;
114+
}
115+
116+
// Check if this is the Dock
117+
NSRunningApplication* app = [NSRunningApplication runningApplicationWithProcessIdentifier:elementPid];
118+
BOOL isDock = app && [[app bundleIdentifier] isEqualToString:@"com.apple.dock"];
49119

50-
void** result = (void**)malloc(childCount * sizeof(void*));
120+
// First, get all children
121+
int totalCount = 0;
122+
void** allChildren = getChildren(element, &totalCount);
51123

52-
for (CFIndex i = 0; i < childCount; i++) {
53-
AXUIElementRef child = (AXUIElementRef)CFArrayGetValueAtIndex(children, i);
54-
CFRetain(child);
55-
result[i] = (void*)child;
124+
if (!allChildren || totalCount == 0) {
125+
*count = 0;
126+
return NULL;
127+
}
128+
129+
// For the Dock, skip all filtering and return all children
130+
if (isDock) {
131+
*count = totalCount;
132+
return allChildren;
133+
}
134+
135+
// For non-Dock apps, continue with normal filtering...
136+
137+
// Try to get the parent element's bounds (if available)
138+
CGRect parentBounds = CGRectZero;
139+
bool hasParentBounds = false;
140+
141+
CFTypeRef posValue = NULL, sizeValue = NULL;
142+
143+
if (AXUIElementCopyAttributeValue(axElement, kAXPositionAttribute, &posValue) == kAXErrorSuccess && posValue) {
144+
if (AXValueGetValue(posValue, kAXValueCGPointType, &parentBounds.origin)) {
145+
if (AXUIElementCopyAttributeValue(axElement, kAXSizeAttribute, &sizeValue) == kAXErrorSuccess && sizeValue) {
146+
if (AXValueGetValue(sizeValue, kAXValueCGSizeType, &parentBounds.size)) {
147+
hasParentBounds = true;
148+
}
149+
CFRelease(sizeValue);
56150
}
151+
}
152+
CFRelease(posValue);
153+
}
154+
155+
// Get screen bounds for all displays
156+
uint32_t displayCount;
157+
CGDirectDisplayID displays[32];
158+
CGGetActiveDisplayList(32, displays, &displayCount);
159+
160+
// Create a union of all display bounds
161+
CGRect allScreensBounds = CGRectZero;
162+
for (uint32_t i = 0; i < displayCount; i++) {
163+
CGRect displayBounds = CGDisplayBounds(displays[i]);
164+
if (i == 0) {
165+
allScreensBounds = displayBounds;
166+
} else {
167+
allScreensBounds = CGRectUnion(allScreensBounds, displayBounds);
168+
}
169+
}
170+
171+
// Temporary array to hold visible children
172+
void** visibleChildren = (void**)malloc(totalCount * sizeof(void*));
173+
int visibleCount = 0;
174+
175+
for (int i = 0; i < totalCount; i++) {
176+
AXUIElementRef child = (AXUIElementRef)allChildren[i];
177+
178+
// Get child's position and size
179+
CGPoint childPos = CGPointZero;
180+
CGSize childSize = CGSizeZero;
181+
bool hasPosition = false, hasSize = false;
182+
183+
CFTypeRef childPosValue = NULL;
184+
if (AXUIElementCopyAttributeValue(child, kAXPositionAttribute, &childPosValue) == kAXErrorSuccess && childPosValue) {
185+
if (AXValueGetValue(childPosValue, kAXValueCGPointType, &childPos)) {
186+
hasPosition = true;
187+
}
188+
CFRelease(childPosValue);
189+
}
190+
191+
CFTypeRef childSizeValue = NULL;
192+
if (AXUIElementCopyAttributeValue(child, kAXSizeAttribute, &childSizeValue) == kAXErrorSuccess && childSizeValue) {
193+
if (AXValueGetValue(childSizeValue, kAXValueCGSizeType, &childSize)) {
194+
hasSize = true;
195+
}
196+
CFRelease(childSizeValue);
197+
}
198+
199+
// If we can't get position/size, include it anyway (might be a container)
200+
if (!hasPosition && !hasSize) {
201+
visibleChildren[visibleCount++] = (void*)child;
202+
continue;
203+
}
57204

58-
CFRelease(visibleChildrenValue);
59-
return result;
205+
// Check if child has non-zero size (if size is available)
206+
if (hasSize && (childSize.width <= 0 || childSize.height <= 0)) {
207+
CFRelease(child);
208+
continue;
60209
}
61210

62-
CFRelease(visibleChildrenValue);
211+
// If we have position, do spatial checks
212+
if (hasPosition && hasSize) {
213+
CGRect childRect = CGRectMake(childPos.x, childPos.y, childSize.width, childSize.height);
214+
215+
// Check if child intersects with any screen bounds
216+
if (!CGRectIntersectsRect(childRect, allScreensBounds)) {
217+
CFRelease(child);
218+
continue;
219+
}
220+
221+
// If parent has bounds, check if child intersects with parent
222+
if (hasParentBounds && !CGRectIntersectsRect(childRect, parentBounds)) {
223+
CFRelease(child);
224+
continue;
225+
}
226+
227+
// Only check occlusion if requested
228+
if (checkOcclusion && isElementOccluded(childRect, elementPid)) {
229+
CFRelease(child);
230+
continue;
231+
}
232+
}
233+
234+
// This child is visible, add it to our result
235+
visibleChildren[visibleCount++] = (void*)child;
63236
}
64237

65-
return getChildren(element, count);
238+
free(allChildren);
239+
240+
if (visibleCount == 0) {
241+
free(visibleChildren);
242+
*count = 0;
243+
return NULL;
244+
}
245+
246+
// Resize array to actual visible count
247+
void** result = (void**)realloc(visibleChildren, visibleCount * sizeof(void*));
248+
*count = visibleCount;
249+
250+
return result ? result : visibleChildren;
66251
}
67252

68253
// Get system-wide accessibility element

0 commit comments

Comments
 (0)