Add macOS (AppKit) support: a headless view-tree walker#740
Draft
markmals wants to merge 7 commits into
Draft
Conversation
FLEXAppKitWalker is the macOS analog of FHSView: it walks NSView subtrees into an immutable FLEXAppKitViewSnapshot per node -- real runtime class via object_getClass, raw frame plus a normalized top-left rect with isFlipped, hidden/alpha/identifier, and a decomposed NSFont (FLEXAppKitFont: raw CoreText weight trait + nearest named weight, never a lossy NSFontManager conversion). Frames normalize against the full window frame via screen conversion, so per-view isFlipped is resolved by AppKit rather than manual y-flipping -- the silent-correctness trap of an AppKit port. Shipped as a separate FLEXAppKit SPM target/product under Classes/ViewHierarchy/AppKit, excluded from the iOS FLEX target, so the iOS build is unaffected and the FLEXCore engine split can land later. DevProbe is a scoped correctness harness: `swift run FLEXAppKitProbe` (13 checks). Package.swift was reindented by tooling; upstream style restored before PR.
Build out FLEXAppKitWalker to the genuinely-AppKit-specific field set the objc runtime cannot compute on its own: - NSApp.windows enumeration as tree roots (key/main/panel + contentView subtree) -- the rooted entry the flat heap enumerator can't provide. - swiftUIBoundary flag at NSHostingView (substring match across the class chain, since NSHostingView<Content> has a mangled Swift name); traversal continues through the layer-backed scaffold below. - FLEXAppKitColor: appearance-resolved sRGB hex (#RRGGBBAA) + catalog name where the color is a catalog color + the appearance context. Resolves under performAsCurrentDrawingAppearance so catalog/dynamic colors do not return nil or throw. - FLEXAppKitLayer: layer facts where the view is layer-backed, plus the PARALLEL CALayer subtree (sublayers incl. standalone ones backing no view). A nil layer stays nil -- a normal case, not an error. - NSVisualEffectView material / blendingMode. Snapshots refactored to a property-set pattern (internal headers) so the immutable record can grow without an unwieldy initializer. DevProbe extended to 26 checks (all pass), incl. the coordinate flip, the layer/color decomposition (#FF0000FF round-trip), and the nil-layer case verified against a standalone view (views inside a modern window are implicitly layer-backed).
Add the remaining domain.node fields the walker produces: superclasses (class chain to NSObject), axRole (accessibilityRole), text (NSControl / NSText displayed string -- what the selector grammar's `text` predicate matches), and constraintsCount (forward constraints on the view; the full both-directions list is reserved for the constraints verb). Add depth-bounded traversal: snapshotForView:inWindow:maxDepth: and snapshotApplicationWindowsWithMaxDepth:. A node at the bound with subviews reports truncated == YES + childCount and omits children; a leaf is never truncated. childCount is always emitted. DevProbe extended (all checks pass).
Add FLEXConstraintNode: NSLayoutConstraints touching a view in BOTH directions (the view's own constraints + ancestor-held constraints that reference it -- AppKit has no public reverse index), each serialized as first.attr (relation) second.attr * multiplier + constant @ priority with per-item class/kind/isTarget, plus intrinsic-sizing facts (translatesAutoresizingMaskIntoConstraints, per-axis hugging/compression, intrinsicContentSize). Cross-platform -- NSLayoutConstraint is one class. Fold in the adversarial review's four confirmed findings: - color: a CGColor (every layer color) is flattened, so appearance resolution and catalog-name capture are impossible on it. Split the path so a CGColor yields baked sRGB hex only, while live NSColor inputs get appearance-resolved hex + catalog name. Header contract corrected. - layer: bound CALayer recursion (cap 64) with truncated + sublayerCount, so pathological deep trees (CATiledLayer/WebKit/Metal) cannot overflow the stack. - windows: only top-level windows are roots; attached sheets and child windows nest under their parent (a visited set guards cycles) -- the nested-transient contract, previously emitted flat. DevProbe now 46 checks (all pass).
snapshotForHitTestAtPoint:inWindow: returns the deepest view at a window- base point as a single node (children omitted) -- the macOS substitute for touch hit-testing (HANDOFF 5.2). DevProbe now 47 checks, all pass.
FLEXAppKitJSON projects the walker's snapshot model into JSON-serializable Foundation dictionaries shaped per the node schema (fixed-precision floats, NSNull for nils, deterministic key order), per ARCHITECTURE 5.3 -- the FLEX side returns Foundation collections and the consumer serializes the string. SampleAppKitDump builds a representative window (sidebar NSVisualEffectView, known fonts, a layer-backed accent row, Auto Layout) and prints its own runtime view tree as JSON via the real walker -- a manual-verification artifact needing no injection or SIP changes: `swift run SampleAppKitDump`. Also fix FLEXConstraintNode: the forward pass now keeps only constraints that TOUCH the view (first or second item == it), excluding constraints a view merely holds between its descendants -- matching the spec's both- directions = touching, which drops AppKit's private internal label constraints from the result.
Scrub flexscope-internal references so the contribution stands alone in FLEX: remove the `// SPEC: domain.walker` reverse-pointers (a flexscope spec-traceability convention) from all FLEXAppKit sources, reword the few prose mentions of domain.walker / the node schema / ARCHITECTURE into self-contained descriptions, and restore Package.swift to FLEX's upstream 4-space style so the diff is just the added FLEXAppKit product/targets.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds macOS (AppKit) support to FLEX as a new, self-contained
FLEXAppKitproduct: a headlessNSViewview-tree walker that produces an immutable, JSON-serializable snapshot of a running app's real view hierarchy — actual runtime class names, frames, fonts, CALayer facts, Auto Layout constraints,NSVisualEffectViewmaterials, and the windows. It's the macOS analog of the existingFHSView/FHSViewSnapshot, built for headless/programmatic use.Packaged as a separate
FLEXAppKitSPM target/product underClasses/ViewHierarchy/AppKit/, excluded from the iOSFLEXtarget. The iOS build is untouched — the only change to the existing target is one addedexcludeentry for a brand-new directory.What's in it
A walker (
FLEXAppKitWalker) → immutable snapshots (FLEXAppKit{ViewSnapshot,WindowSnapshot,Font,Layer,Color},FLEXConstraintNode) capturing per node:object_getClass) + the superclass chain.frameand a normalized top-left, full-window-frame-relative rect andisFlipped, computed via screen-coordinate conversion (so per-viewisFlippedis handled by AppKit, not manual y-flipping — the Fixed some warnings #1 silent-correctness trap of an AppKit port).NSFontdecomposed to family/size/PostScript/traits + the rawNSFontWeightTraitand the nearest named weight (never a lossyNSFontManager1–14 conversion).nillayer is a normal case, not an error) + the parallel CALayer subtree (layer.sublayers ≠ subviews, standalone sublayers included), depth-capped.NSColor; baked sRGB hex forCGColor(layer colors are flattened, so the catalog identity is gone — handled honestly).NSLayoutConstrainttouching a node in both directions + intrinsic-sizing facts (cross-platform, zero#if).material/blendingMode,axRole,identifier,text,hidden/alpha,swiftUIBoundary(flagged atNSHostingView),--depth/truncated,hitTest:at a point, andNSApp.windowsenumeration with sheets/child windows nested under their parent.FLEXAppKitJSONprojects the snapshot into JSON-serializable Foundation dictionaries (fixed precision, deterministic).Verify
Note: FLEX's single
FLEXtarget owns all ofClasses/, soswift testtries to compile the UIKit code on macOS. Until an engine split, the scopedswift runharnesses above are the test path.For your review — scope & follow-ups
FLEXCore/FLEXengine product split — so macOSivars/props/classes(the reflection engine) are a natural follow-up PR. Happy to do that next if you'd like.master, or would you prefer amacosfeature branch?DevProbe/SampleAppKitDump/FLEXAppKitJSONare verification tooling + a JSON convenience — glad to drop or relocate them if you'd prefer a library-only PR.#if TARGET_OS_OSX-guarded.