Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions Classes/ViewHierarchy/AppKit/FLEXAppKitColor.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
//
// FLEXAppKitColor.h
// FLEX
//
// A resolved color fact: the unambiguous sRGB hex snapshot, plus — for live
// NSColor inputs — the catalog/dynamic name where available (what a native
// reimplementation actually uses) and the appearance context it was resolved
// under. A live catalog/dynamic NSColor is resolved through an appearance + a
// concrete color space (otherwise reading components throws or yields the wrong
// appearance).
//
// CGColor inputs (e.g. a CALayer's backgroundColor) are already FLATTENED by the
// time the walker sees them: the dynamic/catalog identity was baked away when the
// view set the layer color, so for a CGColor only `hex` is populated (the baked
// sRGB value); `catalogName` and `appearanceName` are nil and `inAppearance:` is
// a no-op. Read view-level colors as NSColor (not via the layer) to recover the
// catalog name and appearance.
//

#import <Foundation/Foundation.h>
#import <CoreGraphics/CoreGraphics.h>

NS_ASSUME_NONNULL_BEGIN

@interface FLEXAppKitColor : NSObject

/// Resolve an NSColor (or a CGColorRef) under `appearance`. Returns nil only if
/// the input is nil. `appearance` may be nil (resolves under the current default).
+ (nullable FLEXAppKitColor *)colorFromColor:(nullable id)nsColorOrCGColor
inAppearance:(nullable id)appearance;

/// sRGB hex "#RRGGBBAA"; nil if the color could not be resolved to components.
@property (nonatomic, readonly, copy, nullable) NSString *hex;
/// Catalog/dynamic name (e.g. "controlAccentColor") where the color is a catalog
/// color; nil otherwise.
@property (nonatomic, readonly, copy, nullable) NSString *catalogName;
/// The appearance the color was resolved under (e.g. "NSAppearanceNameDarkAqua").
@property (nonatomic, readonly, copy, nullable) NSString *appearanceName;

@end

NS_ASSUME_NONNULL_END
94 changes: 94 additions & 0 deletions Classes/ViewHierarchy/AppKit/FLEXAppKitColor.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
//
// FLEXAppKitColor.m
// FLEX
//

#import "FLEXAppKitColor.h"

#if TARGET_OS_OSX

#import <AppKit/AppKit.h>

@interface FLEXAppKitColor ()
@property (nonatomic, copy, nullable) NSString *hex;
@property (nonatomic, copy, nullable) NSString *catalogName;
@property (nonatomic, copy, nullable) NSString *appearanceName;
@end

/// sRGB hex of a color already converted to sRGB, or nil. The caller must convert
/// first — reading components on a non-RGB color throws.
static NSString *FLEXHexOfSRGBColor(NSColor *srgb) {
if (srgb == nil) {
return nil;
}
int r = (int)lround(srgb.redComponent * 255.0);
int g = (int)lround(srgb.greenComponent * 255.0);
int b = (int)lround(srgb.blueComponent * 255.0);
int a = (int)lround(srgb.alphaComponent * 255.0);
return [NSString stringWithFormat:@"#%02X%02X%02X%02X", r, g, b, a];
}

@implementation FLEXAppKitColor

+ (nullable FLEXAppKitColor *)colorFromColor:(nullable id)input
inAppearance:(nullable id)appearance {
if (input == nil) {
return nil;
}

// CGColor (e.g. a layer's backgroundColor): already a flat, baked color. It
// cannot carry a catalog name and is NOT re-resolvable to a different
// appearance — the dynamic identity was lost when the view baked it into the
// layer. Capture the baked sRGB hex as-is; catalogName/appearanceName stay nil.
if (![input isKindOfClass:[NSColor class]]
&& CFGetTypeID((__bridge CFTypeRef)input) == CGColorGetTypeID()) {
NSColor *flat = [NSColor colorWithCGColor:(__bridge CGColorRef)input];
NSString *hex = FLEXHexOfSRGBColor([flat colorUsingColorSpace:[NSColorSpace sRGBColorSpace]]);
if (hex == nil) {
return nil; // unconvertible (e.g. a pattern color) — no misleading value
}
FLEXAppKitColor *result = [FLEXAppKitColor new];
result.hex = hex;
return result;
}

if (![input isKindOfClass:[NSColor class]]) {
return nil;
}
NSColor *color = input;
FLEXAppKitColor *result = [FLEXAppKitColor new];

// Catalog/dynamic NAME, only where the color genuinely is a catalog color
// (reading colorNameComponent on a non-catalog color throws).
if (@available(macOS 10.14, *)) {
if (color.type == NSColorTypeCatalog) {
result.catalogName = color.colorNameComponent;
}
}

NSAppearance *resolveAppearance = [appearance isKindOfClass:[NSAppearance class]] ? appearance : nil;
result.appearanceName = resolveAppearance.name;

// Resolve to sRGB UNDER the appearance — required for a live catalog/dynamic
// NSColor, which otherwise returns nil or resolves under the wrong appearance.
__block NSColor *resolved = nil;
void (^toSRGB)(void) = ^{
resolved = [color colorUsingColorSpace:[NSColorSpace sRGBColorSpace]];
};
if (resolveAppearance != nil) {
if (@available(macOS 11.0, *)) {
[resolveAppearance performAsCurrentDrawingAppearance:toSRGB];
} else {
toSRGB();
}
} else {
toSRGB();
}

result.hex = FLEXHexOfSRGBColor(resolved);
return result;
}

@end

#endif // TARGET_OS_OSX
32 changes: 32 additions & 0 deletions Classes/ViewHierarchy/AppKit/FLEXAppKitFont.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
//
// FLEXAppKitFont.h
// FLEX
//
// Decomposed NSFont facts read off a font carrier. Emits the raw CoreText
// weight trait AND the nearest named weight — never a lossy NSFontManager
// (1–14) conversion.
//

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface FLEXAppKitFont : NSObject

/// Decompose the font carried by `object` (or its `-cell`), or nil if it carries none.
+ (nullable FLEXAppKitFont *)fontForObject:(id)object;

@property (nonatomic, readonly, copy) NSString *familyName;
@property (nonatomic, readonly) double pointSize;
/// Raw CoreText NSFontWeightTrait, in [-1.0, 1.0]. 0.0 when the descriptor omits it.
@property (nonatomic, readonly) double weightTrait;
/// Nearest named weight ("regular", "semibold", …) to `weightTrait`.
@property (nonatomic, readonly, copy) NSString *weightName;
/// PostScript name (e.g. ".SFNS-Regular"), or nil if unavailable.
@property (nonatomic, readonly, copy, nullable) NSString *postScriptName;
/// Symbolic traits present on the font ("bold", "italic", "monoSpace", …).
@property (nonatomic, readonly, copy) NSArray<NSString *> *traits;

@end

NS_ASSUME_NONNULL_END
113 changes: 113 additions & 0 deletions Classes/ViewHierarchy/AppKit/FLEXAppKitFont.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
//
// FLEXAppKitFont.m
// FLEX
//

#import "FLEXAppKitFont.h"

#if TARGET_OS_OSX

#import <AppKit/AppKit.h>

@interface FLEXAppKitFont ()
@property (nonatomic, copy) NSString *familyName;
@property (nonatomic) double pointSize;
@property (nonatomic) double weightTrait;
@property (nonatomic, copy) NSString *weightName;
@property (nonatomic, copy, nullable) NSString *postScriptName;
@property (nonatomic, copy) NSArray<NSString *> *traits;
@end

/// The font, read off `object` directly or off its `-cell`, or nil. The carrier set is
/// "any object responding to -font" — not a hardcoded class list.
static NSFont *FLEXFontFromCarrier(id object) {
if (object == nil) {
return nil;
}

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
if ([object respondsToSelector:@selector(font)]) {
id font = [object performSelector:@selector(font)];
if ([font isKindOfClass:[NSFont class]]) {
return font;
}
}
if ([object respondsToSelector:@selector(cell)]) {
id cell = [object performSelector:@selector(cell)];
if ([cell respondsToSelector:@selector(font)]) {
id font = [cell performSelector:@selector(font)];
if ([font isKindOfClass:[NSFont class]]) {
return font;
}
}
}
#pragma clang diagnostic pop

return nil;
}

/// Nearest named weight to a raw CoreText trait, using AppKit's own constants so the
/// thresholds track the platform rather than hardcoded folklore numbers.
static NSString *FLEXNearestWeightName(CGFloat weight) {
const struct { CGFloat value; NSString *name; } weights[] = {
{ NSFontWeightUltraLight, @"ultraLight" },
{ NSFontWeightThin, @"thin" },
{ NSFontWeightLight, @"light" },
{ NSFontWeightRegular, @"regular" },
{ NSFontWeightMedium, @"medium" },
{ NSFontWeightSemibold, @"semibold" },
{ NSFontWeightBold, @"bold" },
{ NSFontWeightHeavy, @"heavy" },
{ NSFontWeightBlack, @"black" },
};

NSString *nearest = @"regular";
CGFloat bestDelta = CGFLOAT_MAX;
for (size_t i = 0; i < sizeof(weights) / sizeof(weights[0]); i++) {
CGFloat delta = ABS(weight - weights[i].value);
if (delta < bestDelta) {
bestDelta = delta;
nearest = weights[i].name;
}
}
return nearest;
}

static NSArray<NSString *> *FLEXSymbolicTraitNames(NSFontDescriptorSymbolicTraits traits) {
NSMutableArray<NSString *> *names = [NSMutableArray array];
if (traits & NSFontDescriptorTraitBold) { [names addObject:@"bold"]; }
if (traits & NSFontDescriptorTraitItalic) { [names addObject:@"italic"]; }
if (traits & NSFontDescriptorTraitExpanded) { [names addObject:@"expanded"]; }
if (traits & NSFontDescriptorTraitCondensed) { [names addObject:@"condensed"]; }
if (traits & NSFontDescriptorTraitMonoSpace) { [names addObject:@"monoSpace"]; }
if (traits & NSFontDescriptorTraitVertical) { [names addObject:@"vertical"]; }
if (traits & NSFontDescriptorTraitUIOptimized) { [names addObject:@"uiOptimized"]; }
return names;
}

@implementation FLEXAppKitFont

+ (nullable FLEXAppKitFont *)fontForObject:(id)object {
NSFont *font = FLEXFontFromCarrier(object);
if (font == nil) {
return nil;
}

NSDictionary *traitsDict = [font.fontDescriptor objectForKey:NSFontTraitsAttribute];
NSNumber *weightNumber = traitsDict[NSFontWeightTrait];
CGFloat weight = weightNumber != nil ? weightNumber.doubleValue : 0.0;

FLEXAppKitFont *result = [FLEXAppKitFont new];
result.familyName = font.familyName ?: font.fontName;
result.pointSize = font.pointSize;
result.weightTrait = weight;
result.weightName = FLEXNearestWeightName(weight);
result.postScriptName = font.fontName;
result.traits = FLEXSymbolicTraitNames(font.fontDescriptor.symbolicTraits);
return result;
}

@end

#endif // TARGET_OS_OSX
28 changes: 28 additions & 0 deletions Classes/ViewHierarchy/AppKit/FLEXAppKitJSON.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//
// FLEXAppKitJSON.h
// FLEX
//
// Projects the walker's snapshot model into JSON-serializable Foundation
// dictionaries (floats at fixed precision, nils as NSNull) so the output is
// deterministic and round-trips through NSJSONSerialization. The final string
// serialization is left to the consumer — this returns Foundation collections.
//

#import <Foundation/Foundation.h>

@class FLEXAppKitViewSnapshot;
@class FLEXAppKitWindowSnapshot;
@class FLEXConstraintNode;

NS_ASSUME_NONNULL_BEGIN

@interface FLEXAppKitJSON : NSObject

+ (NSArray<NSDictionary *> *)dictionariesForWindows:(NSArray<FLEXAppKitWindowSnapshot *> *)windows;
+ (NSDictionary *)dictionaryForWindow:(FLEXAppKitWindowSnapshot *)window;
+ (NSDictionary *)dictionaryForView:(FLEXAppKitViewSnapshot *)view;
+ (NSDictionary *)dictionaryForConstraintNode:(FLEXConstraintNode *)node;

@end

NS_ASSUME_NONNULL_END
Loading