@@ -262,14 +262,72 @@ enum AXIntrospection {
262262 . contains ( where: { isChrome ( $0) && frameScreen ( of: $0) . contains ( point) } ) {
263263 return nil
264264 }
265- // Never fall back to the window or application container: escalating to
266- // AXWindow is what made a background click resolve to the whole app.
267- guard let target = nearestIdentified ( in: chain) ?? deepestNonContainer ( in: chain) else {
265+ // Pure positional specificity: the DEEPEST meaningful node at the point
266+ // is the answer. Never the window or application container — escalating
267+ // to AXWindow is what made a background click resolve to the whole app.
268+ guard let target = deepestMeaningful ( in: chain) else {
268269 return nil
269270 }
270271 return element ( for: target, ancestorChain: chain)
271272 }
272273
274+ /// REGION anchor (cli-vtrvt.2): the nearest MEANINGFUL element to a point
275+ /// that hit-tests to nothing (decoration, dividers, gaps beyond any
276+ /// container's frame). Walks the containing window's tree collecting
277+ /// meaningful nodes (same predicate as the hit-test, chrome and
278+ /// window-ghost groups excluded) and returns the one whose frame is
279+ /// closest to the point — smaller frame wins a distance tie, mirroring the
280+ /// specificity rule.
281+ static func regionAnchor( for point: CGPoint ) -> Element ? {
282+ let app = appElement ( )
283+ let windows = elementArray ( app, kAXWindowsAttribute) . filter { !isOverlayWindow( $0) }
284+ guard let window = windows. first ( where: { frameScreen ( of: $0) . contains ( point) } ) else {
285+ return nil
286+ }
287+ let windowFrame = frameScreen ( of: window)
288+ var best : ( element: AXUIElement , distance: CGFloat , area: CGFloat ) ?
289+ collectMeaningful ( in: window, windowFrame: windowFrame, depth: 0 ) { candidate in
290+ let frame = frameScreen ( of: candidate)
291+ let d = distance ( from: point, to: frame)
292+ let a = frame. width * frame. height
293+ if best == nil || d < best!. distance || ( d == best!. distance && a < best!. area) {
294+ best = ( candidate, d, a)
295+ }
296+ }
297+ guard let best else { return nil }
298+ return element ( for: best. element, ancestorChain: ancestorChain ( from: best. element) )
299+ }
300+
301+ private static func collectMeaningful(
302+ in element: AXUIElement ,
303+ windowFrame: CGRect ,
304+ depth: Int ,
305+ _ visit: ( AXUIElement ) -> Void
306+ ) {
307+ guard depth < maxDepth else { return }
308+ for child in elementArray ( element, kAXChildrenAttribute) {
309+ if isChrome ( child) { continue }
310+ let role = string ( child, kAXRoleAttribute) ?? " "
311+ let ghost = role == " AXGroup "
312+ && ( string ( child, kAXIdentifierAttribute) ?? " " ) . isEmpty
313+ && labelText ( child) . isEmpty
314+ && frameScreen ( of: child) . contains ( windowFrame. insetBy ( dx: 8 , dy: 8 ) )
315+ if !ghost, isMeaningful ( child, role: role) {
316+ let frame = frameScreen ( of: child)
317+ if frame. width > 0 , frame. height > 0 { visit ( child) }
318+ }
319+ collectMeaningful ( in: child, windowFrame: windowFrame, depth: depth + 1 , visit)
320+ }
321+ }
322+
323+ /// Euclidean distance from `point` to the nearest edge of `rect` (0 when
324+ /// the rect contains the point).
325+ private static func distance( from point: CGPoint , to rect: CGRect ) -> CGFloat {
326+ let dx = max ( rect. minX - point. x, 0 , point. x - rect. maxX)
327+ let dy = max ( rect. minY - point. y, 0 , point. y - rect. maxY)
328+ return ( dx * dx + dy * dy) . squareRoot ( )
329+ }
330+
273331 /// Geometric hit-test beneath the overlay. The native point query cannot see
274332 /// past our own full-window overlay panel, so descend the frontmost
275333 /// non-overlay window whose frame contains `point` to the deepest descendant
@@ -298,7 +356,15 @@ enum AXIntrospection {
298356 let frame = frameScreen ( of: $0)
299357 return frame. width > 0 && frame. height > 0 && frame. contains ( point)
300358 }
301- . min { area ( of: $0) < area ( of: $1) }
359+ . min { lhs, rhs in
360+ let ( lhsArea, rhsArea) = ( area ( of: lhs) , area ( of: rhs) )
361+ guard lhsArea == rhsArea else { return lhsArea < rhsArea }
362+ // Equal-area tie (e.g. a materialized region coextensive with a
363+ // card's seeded surface): CONTENT beats the surface — seeded
364+ // surfaces present as AXUnknown leaves and are by construction
365+ // the LEAST specific thing at their level.
366+ return surfaceRank ( of: lhs) < surfaceRank ( of: rhs)
367+ }
302368 guard let candidate else { return element }
303369 return deepestChild ( of: candidate, containing: point, depth: depth + 1 )
304370 }
@@ -308,6 +374,10 @@ enum AXIntrospection {
308374 return frame. width * frame. height
309375 }
310376
377+ private static func surfaceRank( of element: AXUIElement ) -> Int {
378+ string ( element, kAXRoleAttribute) == " AXUnknown " ? 1 : 0
379+ }
380+
311381 /// Climb `kAXParentAttribute` from `element` up to the window, returning the
312382 /// chain ordered root-first.
313383 private static func ancestorChain( from element: AXUIElement ) -> [ AXUIElement ] {
@@ -352,33 +422,21 @@ enum AXIntrospection {
352422 isWindowChrome ( subrole: string ( element, kAXSubroleAttribute) ?? " " )
353423 }
354424
355- private static func nearestIdentified( in rootFirstChain: [ AXUIElement ] ) -> AXUIElement ? {
356- for element in rootFirstChain. reversed ( ) {
357- let role = string ( element, kAXRoleAttribute) ?? " "
358- if role == " AXWindow " || role == " AXApplication " { continue }
359- let identifier = string ( element, kAXIdentifierAttribute) ?? " "
360- let label = labelText ( element)
361- let actionable = actionNames ( element) . contains ( kAXPressAction as String )
362- || actionableRoles. contains ( role)
363- if actionable || !identifier. isEmpty || ( !label. isEmpty && role != " AXGroup " ) {
364- return element
365- }
366- }
367- return nil
368- }
369-
370- /// Deepest element in the chain that is still a plausible target — anything
371- /// that is not the window or application container. Used only when no
372- /// identified/actionable ancestor exists, so a click on a plain leaf resolves
373- /// to that leaf rather than escalating to the whole window (which would then
374- /// show the window title in the composer header).
375- private static func deepestNonContainer( in rootFirstChain: [ AXUIElement ] ) -> AXUIElement ? {
376- // A structural, unidentified group that spans (nearly) the whole window is
377- // the window in disguise — NSHostingView's root AXGroup covers the full
378- // window frame, so falling back to it is the same "background click
379- // highlights the whole app" bug the window/application skip guards
380- // against. Detect it geometrically: the group's frame swallows the
381- // window's frame minus a small inset.
425+ /// The MOST SPECIFIC annotation target at the hit: walk the chain
426+ /// deepest-first and return the first MEANINGFUL node (pure positional
427+ /// specificity — the button on a card wins at the button, the card's
428+ /// surface at the card's padding, the section at the section's padding;
429+ /// there is deliberately NO depth-walking UI).
430+ ///
431+ /// A meaningless wrapper between content and its meaningful ancestor is
432+ /// passed through: by containment, that ancestor is still the most
433+ /// specific meaningful node at the point. Skipped entirely: the window and
434+ /// application containers (a background click must never resolve to the
435+ /// whole app) and structural, unidentified, content-less groups spanning
436+ /// (nearly) the whole window — NSHostingView's root AXGroup is the window
437+ /// in disguise, detected geometrically because its frame swallows the
438+ /// window's frame minus a small inset.
439+ private static func deepestMeaningful( in rootFirstChain: [ AXUIElement ] ) -> AXUIElement ? {
382440 let windowFrame = rootFirstChain. first { string ( $0, kAXRoleAttribute) == " AXWindow " }
383441 . map ( frameScreen ( of: ) )
384442 for element in rootFirstChain. reversed ( ) {
@@ -391,11 +449,25 @@ enum AXIntrospection {
391449 frameScreen ( of: element) . contains ( windowFrame. insetBy ( dx: 8 , dy: 8 ) ) {
392450 continue
393451 }
394- return element
452+ if isMeaningful ( element , role : role ) { return element }
395453 }
396454 return nil
397455 }
398456
457+ /// Whether a node is an annotation target in its own right: an identifier,
458+ /// a title/description label, a string VALUE, or an action. Counting
459+ /// AXValue is load-bearing: plain SwiftUI `Text` materializes AXStaticText
460+ /// with the string in AXValue and EMPTY title/description, so a
461+ /// title-only notion of "labeled" walks straight past every text leaf and
462+ /// resolves its identified ancestor (the whole page) instead.
463+ private static func isMeaningful( _ element: AXUIElement , role: String ) -> Bool {
464+ if !( string ( element, kAXIdentifierAttribute) ?? " " ) . isEmpty { return true }
465+ if !labelText( element) . isEmpty { return true }
466+ if !( string ( element, kAXValueAttribute) ?? " " ) . isEmpty { return true }
467+ return actionNames ( element) . contains ( kAXPressAction as String )
468+ || actionableRoles. contains ( role)
469+ }
470+
399471 /// Build a public ``Element`` for `target`, computing its path from the
400472 /// supplied root-first ancestor chain (with same-role sibling indices).
401473 private static func element( for target: AXUIElement , ancestorChain rootFirst: [ AXUIElement ] ) -> Element {
0 commit comments