From 517104feb4cb001d9ab600a1dd32f20c0e73b594 Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Tue, 28 Jul 2026 12:44:43 -0700 Subject: [PATCH 1/4] feat(macos): add narrow RCTUIButton action bridge Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Libraries/RCTUIKit/RCTUIButton.h | 73 +++++++ .../Libraries/RCTUIKit/RCTUIButton.m | 197 ++++++++++++++++++ .../ReactApple/Libraries/RCTUIKit/RCTUIKit.h | 1 + 3 files changed, 271 insertions(+) create mode 100644 packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUIButton.h create mode 100644 packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUIButton.m diff --git a/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUIButton.h b/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUIButton.h new file mode 100644 index 00000000000..4f97eacf107 --- /dev/null +++ b/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUIButton.h @@ -0,0 +1,73 @@ +/* + * Copyright (c) Microsoft Corporation. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +// [macOS] + +#pragma once + +#include + +#import "RCTUIKitCompat.h" + +#if !TARGET_OS_OSX +#import +#else +#import +#endif + +NS_ASSUME_NONNULL_BEGIN + +#if !TARGET_OS_OSX + +@compatibility_alias RCTUIButton UIButton; +#define RCTUIControlStateNormal UIControlStateNormal +#define RCTUIControlStateHighlighted UIControlStateHighlighted +typedef UIControlState RCTUIControlState; + +#else // TARGET_OS_OSX [ + +typedef NS_OPTIONS(NSUInteger, RCTUIControlState) { + RCTUIControlStateNormal = 0, + RCTUIControlStateHighlighted = 1 << 0, +}; + +@interface RCTUIButtonTitleProxy : NSObject + +@property (nonatomic, strong, nullable) UIFont *font; +@property (nonatomic, assign) NSLineBreakMode lineBreakMode; +@property (nonatomic, assign) NSTextAlignment textAlignment; + +@end + +@interface RCTUIButton : NSButton + +@property (nonatomic, readonly) RCTUIButtonTitleProxy *titleLabel; +@property (nonatomic, copy, nullable) RCTUIColor *backgroundColor; + +- (void)setTitle:(nullable NSString *)title forState:(RCTUIControlState)state; +- (void)setTitleColor:(nullable RCTUIColor *)color forState:(RCTUIControlState)state; + +@end + +#endif // ] TARGET_OS_OSX + +typedef void (^RCTUIActionHandler)(void); + +@interface RCTUIAction : NSObject + ++ (instancetype)actionWithHandler:(RCTUIActionHandler)handler; +@property (nonatomic, readonly, copy) RCTUIActionHandler handler; + +@end + +@interface RCTUIButton (RCTUIAction) + +- (void)rct_setPrimaryAction:(RCTUIAction *)action; + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUIButton.m b/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUIButton.m new file mode 100644 index 00000000000..d01c39f7fb4 --- /dev/null +++ b/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUIButton.m @@ -0,0 +1,197 @@ +/* + * Copyright (c) Microsoft Corporation. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +// [macOS] + +#import "RCTUIButton.h" + +#import + +@interface RCTUIAction () + +- (instancetype)initWithHandler:(RCTUIActionHandler)handler; +- (void)invoke; + +@end + +@implementation RCTUIAction + ++ (instancetype)actionWithHandler:(RCTUIActionHandler)handler +{ + return [[self alloc] initWithHandler:handler]; +} + +- (instancetype)initWithHandler:(RCTUIActionHandler)handler +{ + if (self = [super init]) { + _handler = [handler copy]; + } + return self; +} + +- (void)invoke +{ + self.handler(); +} + +@end + +#if TARGET_OS_OSX + +@class RCTUIButton; + +@interface RCTUIButtonTitleProxy () + +@property (nonatomic, weak) RCTUIButton *button; + +- (instancetype)initWithButton:(RCTUIButton *)button; + +@end + +@interface RCTUIButton () + +- (void)updateAttributedTitles; + +@end + +@implementation RCTUIButtonTitleProxy + +- (instancetype)initWithButton:(RCTUIButton *)button +{ + if (self = [super init]) { + _button = button; + _font = button.font; + _lineBreakMode = NSLineBreakByClipping; + _textAlignment = NSTextAlignmentNatural; + } + return self; +} + +- (void)setFont:(UIFont *)font +{ + _font = font; + [self.button updateAttributedTitles]; +} + +- (void)setLineBreakMode:(NSLineBreakMode)lineBreakMode +{ + _lineBreakMode = lineBreakMode; + [self.button updateAttributedTitles]; +} + +- (void)setTextAlignment:(NSTextAlignment)textAlignment +{ + _textAlignment = textAlignment; + [self.button updateAttributedTitles]; +} + +@end + +@implementation RCTUIButton { + NSString *_normalTitle; + NSString *_highlightedTitle; + RCTUIColor *_normalTitleColor; + RCTUIColor *_highlightedTitleColor; +} + +- (instancetype)initWithFrame:(NSRect)frameRect +{ + if (self = [super initWithFrame:frameRect]) { + _titleLabel = [[RCTUIButtonTitleProxy alloc] initWithButton:self]; + } + return self; +} + +- (instancetype)initWithCoder:(NSCoder *)coder +{ + if (self = [super initWithCoder:coder]) { + _titleLabel = [[RCTUIButtonTitleProxy alloc] initWithButton:self]; + } + return self; +} + +- (void)setTitle:(NSString *)title forState:(RCTUIControlState)state +{ + if (state == RCTUIControlStateHighlighted) { + _highlightedTitle = [title copy]; + } else { + _normalTitle = [title copy]; + } + [self updateAttributedTitles]; +} + +- (void)setTitleColor:(RCTUIColor *)color forState:(RCTUIControlState)state +{ + if (state == RCTUIControlStateHighlighted) { + _highlightedTitleColor = [color copy]; + } else { + _normalTitleColor = [color copy]; + } + [self updateAttributedTitles]; +} + +- (void)setBackgroundColor:(RCTUIColor *)backgroundColor +{ + _backgroundColor = [backgroundColor copy]; + self.wantsLayer = YES; + self.layer.backgroundColor = backgroundColor.CGColor; + self.bordered = NO; +} + +- (void)updateAttributedTitles +{ + self.attributedTitle = [self attributedTitleForTitle:_normalTitle ?: @"" + color:_normalTitleColor]; + self.attributedAlternateTitle = [self attributedTitleForTitle:_highlightedTitle ?: _normalTitle ?: @"" + color:_highlightedTitleColor ?: _normalTitleColor]; +} + +- (NSAttributedString *)attributedTitleForTitle:(NSString *)title color:(RCTUIColor *)color +{ + NSMutableDictionary *attributes = [NSMutableDictionary new]; + if (color != nil) { + attributes[NSForegroundColorAttributeName] = color; + } + if (self.titleLabel.font != nil) { + attributes[NSFontAttributeName] = self.titleLabel.font; + } + + NSMutableParagraphStyle *paragraphStyle = [NSMutableParagraphStyle new]; + paragraphStyle.lineBreakMode = self.titleLabel.lineBreakMode; + paragraphStyle.alignment = self.titleLabel.textAlignment; + attributes[NSParagraphStyleAttributeName] = paragraphStyle; + + return [[NSAttributedString alloc] initWithString:title attributes:attributes]; +} + +@end + +#endif + +@implementation RCTUIButton (RCTUIAction) + +- (void)rct_setPrimaryAction:(RCTUIAction *)action +{ +#if !TARGET_OS_OSX + RCTUIAction *previousAction = objc_getAssociatedObject(self, @selector(rct_setPrimaryAction:)); + if (previousAction != nil) { + [self removeTarget:previousAction action:@selector(invoke) forControlEvents:UIControlEventTouchUpInside]; + } +#endif + + objc_setAssociatedObject( + self, @selector(rct_setPrimaryAction:), action, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + +#if !TARGET_OS_OSX + [self addTarget:action action:@selector(invoke) forControlEvents:UIControlEventTouchUpInside]; +#else + self.target = action; + self.action = @selector(invoke); +#endif +} + +@end diff --git a/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUIKit.h b/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUIKit.h index e1a2f0ae022..7d51cf2dfe7 100644 --- a/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUIKit.h +++ b/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUIKit.h @@ -17,6 +17,7 @@ #import "RCTUIScrollView.h" #import "RCTUISlider.h" #import "RCTUITableView.h" +#import "RCTUIButton.h" #import "RCTUILabel.h" #import "RCTUISwitch.h" #import "RCTUIActivityIndicatorView.h" From 8af36389a3ed2b7056b6e7aab7c23b96d64b81f0 Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Fri, 24 Jul 2026 19:21:20 -0700 Subject: [PATCH 2/4] feat(macos): port RedBox 2.0 to AppKit Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../RCTRedBox2AnsiParser+Internal.h | 13 +- .../React/CoreModules/RCTRedBox2AnsiParser.mm | 15 +- .../RCTRedBox2Controller+Internal.h | 6 +- .../React/CoreModules/RCTRedBox2Controller.mm | 554 +++++++++++++++++- 4 files changed, 578 insertions(+), 10 deletions(-) diff --git a/packages/react-native/React/CoreModules/RCTRedBox2AnsiParser+Internal.h b/packages/react-native/React/CoreModules/RCTRedBox2AnsiParser+Internal.h index 3f6a63e67a2..4fa60a55adc 100644 --- a/packages/react-native/React/CoreModules/RCTRedBox2AnsiParser+Internal.h +++ b/packages/react-native/React/CoreModules/RCTRedBox2AnsiParser+Internal.h @@ -5,10 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import - -#if !TARGET_OS_OSX // [macOS] -#import +#import // [macOS] /** * Parses ANSI escape sequences in text and produces an NSAttributedString @@ -18,10 +15,14 @@ */ @interface RCTRedBox2AnsiParser : NSObject +#if !TARGET_OS_OSX // [macOS] + (NSAttributedString *)attributedStringFromAnsiText:(NSString *)text baseFont:(UIFont *)font baseColor:(UIColor *)color; +#else // [macOS ++ (NSAttributedString *)attributedStringFromAnsiText:(NSString *)text + baseFont:(UIFont *)font + baseColor:(RCTPlatformColor *)color; +#endif // macOS] @end - -#endif // [macOS] diff --git a/packages/react-native/React/CoreModules/RCTRedBox2AnsiParser.mm b/packages/react-native/React/CoreModules/RCTRedBox2AnsiParser.mm index a7a75b93c1f..ba3c39798ce 100644 --- a/packages/react-native/React/CoreModules/RCTRedBox2AnsiParser.mm +++ b/packages/react-native/React/CoreModules/RCTRedBox2AnsiParser.mm @@ -10,19 +10,32 @@ #import #import -#if RCT_DEV_MENU && !TARGET_OS_OSX // [macOS] +#if RCT_DEV_MENU using facebook::react::unstable_redbox::AnsiColor; using facebook::react::unstable_redbox::parseAnsi; +#if !TARGET_OS_OSX // [macOS] static UIColor *RCTUIColorFromAnsiColor(const AnsiColor &c) { return [UIColor colorWithRed:c.r / 255.0 green:c.g / 255.0 blue:c.b / 255.0 alpha:1.0]; } +#else // [macOS +static RCTPlatformColor *RCTUIColorFromAnsiColor(const AnsiColor &c) +{ + return [RCTPlatformColor colorWithRed:c.r / 255.0 green:c.g / 255.0 blue:c.b / 255.0 alpha:1.0]; +} +#endif // macOS] @implementation RCTRedBox2AnsiParser +#if !TARGET_OS_OSX // [macOS] + (NSAttributedString *)attributedStringFromAnsiText:(NSString *)text baseFont:(UIFont *)font baseColor:(UIColor *)color +#else // [macOS ++ (NSAttributedString *)attributedStringFromAnsiText:(NSString *)text + baseFont:(UIFont *)font + baseColor:(RCTPlatformColor *)color +#endif // macOS] { if (text == nil) { return [[NSAttributedString alloc] init]; diff --git a/packages/react-native/React/CoreModules/RCTRedBox2Controller+Internal.h b/packages/react-native/React/CoreModules/RCTRedBox2Controller+Internal.h index 56fb86f387b..7339d6dc010 100644 --- a/packages/react-native/React/CoreModules/RCTRedBox2Controller+Internal.h +++ b/packages/react-native/React/CoreModules/RCTRedBox2Controller+Internal.h @@ -9,11 +9,15 @@ #import "RCTRedBox+Internal.h" -#if RCT_DEV_MENU && !TARGET_OS_OSX // [macOS] +#if RCT_DEV_MENU typedef void (^RCTRedBox2ButtonPressHandler)(void); +#if !TARGET_OS_OSX // [macOS] @interface RCTRedBox2Controller : UIViewController +#else // [macOS +@interface RCTRedBox2Controller : NSViewController +#endif // macOS] @property (nonatomic, weak) id actionDelegate; diff --git a/packages/react-native/React/CoreModules/RCTRedBox2Controller.mm b/packages/react-native/React/CoreModules/RCTRedBox2Controller.mm index 4fa0cd0b610..1be4b3b7cc8 100644 --- a/packages/react-native/React/CoreModules/RCTRedBox2Controller.mm +++ b/packages/react-native/React/CoreModules/RCTRedBox2Controller.mm @@ -13,6 +13,9 @@ #import #include +#if TARGET_OS_OSX // [macOS +#import +#endif // macOS] #import "RCTJscSafeUrl+Internal.h" #import "RCTRedBox2AnsiParser+Internal.h" @@ -22,11 +25,12 @@ // @lint-ignore-every CLANGTIDY clang-diagnostic-switch-default // NOTE: clang-diagnostic-switch-default conflicts with clang-diagnostic-switch-enum -#if RCT_DEV_MENU && !TARGET_OS_OSX // [macOS] +#if RCT_DEV_MENU #pragma mark - RCTRedBox2Controller // Color Palette (matching LogBoxStyle.js) +#if !TARGET_OS_OSX // [macOS] static UIColor *RCTRedBox2BackgroundColor() { return [UIColor colorWithRed:51.0 / 255 green:51.0 / 255 blue:51.0 / 255 alpha:1.0]; @@ -41,6 +45,22 @@ { return [UIColor colorWithWhite:1.0 alpha:opacity]; } +#else // [macOS +static RCTUIColor *RCTRedBox2BackgroundColor() +{ + return [RCTUIColor colorWithRed:51.0 / 255 green:51.0 / 255 blue:51.0 / 255 alpha:1.0]; +} + +static RCTUIColor *RCTRedBox2ErrorColor() +{ + return [RCTUIColor colorWithRed:243.0 / 255 green:83.0 / 255 blue:105.0 / 255 alpha:1.0]; +} + +static RCTUIColor *RCTRedBox2TextColor(CGFloat opacity) +{ + return [RCTUIColor colorWithWhite:1.0 alpha:opacity]; +} +#endif // macOS] enum class Section : uint8_t { Message, CodeFrame, CallStack, kMaxValue }; static constexpr size_t kSectionCount = static_cast(Section::kMaxValue); @@ -50,11 +70,51 @@ }; static const NSTimeInterval kAutoRetryInterval = 20.0; +#if TARGET_OS_OSX // [macOS +// AppKit has no per-control block-based action, so associate the handler with the button. +@interface NSButton (RCTRedBox2) +@property (nonatomic) RCTRedBox2ButtonPressHandler rb2_handler; +- (void)rb2_addBlock:(RCTRedBox2ButtonPressHandler)handler; +@end + +@implementation NSButton (RCTRedBox2) + +- (RCTRedBox2ButtonPressHandler)rb2_handler +{ + return objc_getAssociatedObject(self, @selector(rb2_handler)); +} + +- (void)setRb2_handler:(RCTRedBox2ButtonPressHandler)rb2_handler +{ + objc_setAssociatedObject(self, @selector(rb2_handler), rb2_handler, OBJC_ASSOCIATION_RETAIN_NONATOMIC); +} + +- (void)rb2_callBlock +{ + if (self.rb2_handler) { + self.rb2_handler(); + } +} + +- (void)rb2_addBlock:(RCTRedBox2ButtonPressHandler)handler +{ + self.rb2_handler = handler; + [self setTarget:self]; + [self setAction:@selector(rb2_callBlock)]; +} + +@end +#endif // macOS] @implementation RCTRedBox2Controller { +#if !TARGET_OS_OSX // [macOS] UITableView *_stackTraceTableView; UILabel *_headerTitleLabel; UILabel *_errorCategoryLabel; +#else // [macOS + NSTableView *_stackTraceTableView; + NSTextField *_headerTitleLabel; +#endif // macOS] NSString *_lastErrorMessage; NSArray *_lastStackTrace; NSArray *_customButtonTitles; @@ -64,7 +124,11 @@ @implementation RCTRedBox2Controller { std::array _sectionStates; NSTimer *_autoRetryTimer; NSInteger _autoRetryCountdown; +#if !TARGET_OS_OSX // [macOS] UIButton *_reloadButton; +#else // [macOS + NSButton *_reloadButton; +#endif // macOS] NSString *_reloadBaseText; RCTRedBoxHMRClient *_hmrClient; } @@ -77,7 +141,9 @@ - (instancetype)initWithCustomButtonTitles:(NSArray *)customButtonTi _lastErrorCookie = -1; _customButtonTitles = customButtonTitles; _customButtonHandlers = customButtonHandlers; +#if !TARGET_OS_OSX // [macOS] self.modalPresentationStyle = UIModalPresentationFullScreen; +#endif // [macOS] } return self; } @@ -85,15 +151,29 @@ - (instancetype)initWithCustomButtonTitles:(NSArray *)customButtonTi - (void)viewDidLoad { [super viewDidLoad]; +#if !TARGET_OS_OSX // [macOS] self.view.backgroundColor = RCTRedBox2BackgroundColor(); +#else // [macOS + self.view.wantsLayer = YES; + self.view.layer.backgroundColor = RCTRedBox2BackgroundColor().CGColor; +#endif // macOS] // Header bar (adds itself to self.view) +#if !TARGET_OS_OSX // [macOS] UIView *headerBar = [self createHeaderBar]; +#else // [macOS + RCTPlatformView *headerBar = [self createHeaderBar]; +#endif // macOS] // Footer button bar +#if !TARGET_OS_OSX // [macOS] UIView *footerBar = [self createFooterBar]; +#else // [macOS + RCTPlatformView *footerBar = [self createFooterBar]; +#endif // macOS] // Stack trace table +#if !TARGET_OS_OSX // [macOS] _stackTraceTableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain]; _stackTraceTableView.translatesAutoresizingMaskIntoConstraints = NO; _stackTraceTableView.delegate = self; @@ -112,10 +192,43 @@ - (void)viewDidLoad [_stackTraceTableView.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor], [_stackTraceTableView.bottomAnchor constraintEqualToAnchor:footerBar.topAnchor], ]]; +#else // [macOS + NSScrollView *scrollView = [[NSScrollView alloc] initWithFrame:NSZeroRect]; + scrollView.translatesAutoresizingMaskIntoConstraints = NO; + scrollView.drawsBackground = NO; + scrollView.hasVerticalScroller = YES; + + _stackTraceTableView = [[NSTableView alloc] initWithFrame:NSZeroRect]; + _stackTraceTableView.translatesAutoresizingMaskIntoConstraints = NO; + _stackTraceTableView.dataSource = self; + _stackTraceTableView.delegate = self; + _stackTraceTableView.headerView = nil; + _stackTraceTableView.backgroundColor = [NSColor clearColor]; + _stackTraceTableView.selectionHighlightStyle = NSTableViewSelectionHighlightStyleNone; + _stackTraceTableView.usesAutomaticRowHeights = YES; + _stackTraceTableView.intercellSpacing = NSMakeSize(0, 0); + _stackTraceTableView.allowsColumnReordering = NO; + _stackTraceTableView.allowsColumnResizing = NO; + _stackTraceTableView.columnAutoresizingStyle = NSTableViewFirstColumnOnlyAutoresizingStyle; + + NSTableColumn *tableColumn = [[NSTableColumn alloc] initWithIdentifier:@"info"]; + [_stackTraceTableView addTableColumn:tableColumn]; + + scrollView.documentView = _stackTraceTableView; + [self.view addSubview:scrollView]; + + [NSLayoutConstraint activateConstraints:@[ + [scrollView.topAnchor constraintEqualToAnchor:headerBar.bottomAnchor], + [scrollView.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor], + [scrollView.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor], + [scrollView.bottomAnchor constraintEqualToAnchor:footerBar.topAnchor], + ]]; +#endif // macOS] } #pragma mark - Header Bar +#if !TARGET_OS_OSX // [macOS] - (UIView *)createHeaderBar { UIView *headerContainer = [[UIView alloc] init]; @@ -144,9 +257,40 @@ - (UIView *)createHeaderBar return headerContainer; } +#else // [macOS +- (RCTPlatformView *)createHeaderBar +{ + NSView *headerContainer = [[NSView alloc] init]; + headerContainer.translatesAutoresizingMaskIntoConstraints = NO; + headerContainer.wantsLayer = YES; + headerContainer.layer.backgroundColor = RCTRedBox2ErrorColor().CGColor; + + _headerTitleLabel = [self makeLabel]; + _headerTitleLabel.textColor = [NSColor whiteColor]; + _headerTitleLabel.font = [NSFont systemFontOfSize:16 weight:NSFontWeightSemibold]; + _headerTitleLabel.alignment = NSTextAlignmentCenter; + [headerContainer addSubview:_headerTitleLabel]; + + [self.view addSubview:headerContainer]; + + [NSLayoutConstraint activateConstraints:@[ + [headerContainer.topAnchor constraintEqualToAnchor:self.view.topAnchor], + [headerContainer.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor], + [headerContainer.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor], + + [_headerTitleLabel.leadingAnchor constraintEqualToAnchor:headerContainer.leadingAnchor constant:12], + [_headerTitleLabel.trailingAnchor constraintEqualToAnchor:headerContainer.trailingAnchor constant:-12], + [_headerTitleLabel.bottomAnchor constraintEqualToAnchor:headerContainer.bottomAnchor constant:-12], + [_headerTitleLabel.topAnchor constraintEqualToAnchor:headerContainer.topAnchor constant:12], + ]]; + + return headerContainer; +} +#endif // macOS] #pragma mark - Footer Bar +#if !TARGET_OS_OSX // [macOS] - (UIView *)createFooterBar { const CGFloat buttonHeight = 48; @@ -205,7 +349,70 @@ - (UIView *)createFooterBar return buttonStackView; } +#else // [macOS +- (RCTPlatformView *)createFooterBar +{ + const CGFloat buttonHeight = 48; + + NSString *reloadText = @"Reload"; + NSString *dismissText = @"Dismiss"; + NSString *copyText = @"Copy"; + + NSButton *dismissButton = [self footerButton:dismissText + accessibilityIdentifier:@"redbox-dismiss" + selector:@selector(dismiss)]; + [dismissButton setKeyEquivalent:@"\e"]; + _reloadBaseText = reloadText; + _reloadButton = [self footerButton:reloadText accessibilityIdentifier:@"redbox-reload" selector:@selector(reload)]; + [_reloadButton setKeyEquivalent:@"r"]; + [_reloadButton setKeyEquivalentModifierMask:NSEventModifierFlagCommand]; + NSButton *copyButton = [self footerButton:copyText + accessibilityIdentifier:@"redbox-copy" + selector:@selector(copyStack)]; + [copyButton setKeyEquivalent:@"c"]; + [copyButton setKeyEquivalentModifierMask:NSEventModifierFlagOption | NSEventModifierFlagCommand]; + NSStackView *buttonStackView = [[NSStackView alloc] init]; + buttonStackView.translatesAutoresizingMaskIntoConstraints = NO; + buttonStackView.orientation = NSUserInterfaceLayoutOrientationHorizontal; + buttonStackView.distribution = NSStackViewDistributionFillEqually; + buttonStackView.alignment = NSLayoutAttributeCenterY; + buttonStackView.wantsLayer = YES; + buttonStackView.layer.backgroundColor = RCTRedBox2BackgroundColor().CGColor; + + [buttonStackView addArrangedSubview:dismissButton]; + [buttonStackView addArrangedSubview:_reloadButton]; + [buttonStackView addArrangedSubview:copyButton]; + + for (NSUInteger i = 0; i < [_customButtonTitles count]; i++) { + NSButton *button = [self footerButton:_customButtonTitles[i] + accessibilityIdentifier:@"" + handler:_customButtonHandlers[i]]; + [buttonStackView addArrangedSubview:button]; + } + + // Shadow layer above footer + buttonStackView.layer.shadowColor = [NSColor blackColor].CGColor; + buttonStackView.layer.shadowOffset = CGSizeMake(0, -2); + buttonStackView.layer.shadowRadius = 2; + buttonStackView.layer.shadowOpacity = 0.5; + + [self.view addSubview:buttonStackView]; + + CGFloat bottomInset = [self bottomSafeViewHeight]; + + [NSLayoutConstraint activateConstraints:@[ + [buttonStackView.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor], + [buttonStackView.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor], + [buttonStackView.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor], + [buttonStackView.heightAnchor constraintEqualToConstant:buttonHeight + bottomInset], + ]]; + + return buttonStackView; +} +#endif // macOS] + +#if !TARGET_OS_OSX // [macOS] - (UIButton *)styledButton:(NSString *)title accessibilityIdentifier:(NSString *)accessibilityIdentifier { UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom]; @@ -239,10 +446,68 @@ - (UIButton *)footerButton:(NSString *)title forControlEvents:UIControlEventTouchUpInside]; return button; } +#else // [macOS +- (NSTextField *)makeLabel +{ + NSTextField *label = [[NSTextField alloc] initWithFrame:NSZeroRect]; + label.translatesAutoresizingMaskIntoConstraints = NO; + label.drawsBackground = NO; + label.bezeled = NO; + label.editable = NO; + label.selectable = NO; + label.lineBreakMode = NSLineBreakByWordWrapping; + label.maximumNumberOfLines = 0; + return label; +} + +- (NSAttributedString *)attributedButtonTitle:(NSString *)title +{ + NSMutableParagraphStyle *paragraphStyle = [NSMutableParagraphStyle new]; + paragraphStyle.alignment = NSTextAlignmentCenter; + return [[NSAttributedString alloc] initWithString:title + attributes:@{ + NSForegroundColorAttributeName : [NSColor whiteColor], + NSFontAttributeName : [NSFont systemFontOfSize:14], + NSParagraphStyleAttributeName : paragraphStyle, + }]; +} + +- (NSButton *)styledButton:(NSString *)title accessibilityIdentifier:(NSString *)accessibilityIdentifier +{ + NSButton *button = [[NSButton alloc] initWithFrame:NSZeroRect]; + button.translatesAutoresizingMaskIntoConstraints = NO; + button.accessibilityIdentifier = accessibilityIdentifier; + button.bordered = NO; + button.wantsLayer = YES; + button.layer.backgroundColor = RCTRedBox2BackgroundColor().CGColor; + [button setButtonType:NSButtonTypeMomentaryPushIn]; + button.attributedTitle = [self attributedButtonTitle:title]; + return button; +} + +- (NSButton *)footerButton:(NSString *)title + accessibilityIdentifier:(NSString *)accessibilityIdentifier + selector:(SEL)selector +{ + NSButton *button = [self styledButton:title accessibilityIdentifier:accessibilityIdentifier]; + button.target = self; + button.action = selector; + return button; +} + +- (NSButton *)footerButton:(NSString *)title + accessibilityIdentifier:(NSString *)accessibilityIdentifier + handler:(RCTRedBox2ButtonPressHandler)handler +{ + NSButton *button = [self styledButton:title accessibilityIdentifier:accessibilityIdentifier]; + [button rb2_addBlock:handler]; + return button; +} +#endif // macOS] - (CGFloat)bottomSafeViewHeight { -#if TARGET_OS_MACCATALYST +#if TARGET_OS_MACCATALYST || TARGET_OS_OSX // [macOS] return 0; #else return RCTKeyWindow().safeAreaInsets.bottom; @@ -289,15 +554,25 @@ - (void)showErrorMessage:(NSString *)message [_stackTraceTableView reloadData]; if (!isRootViewControllerPresented) { +#if !TARGET_OS_OSX // [macOS] [RCTKeyWindow().rootViewController presentViewController:self animated:NO completion:nil]; +#else // [macOS + [[RCTKeyWindow() contentViewController] presentViewControllerAsSheet:self]; +#endif // macOS] } // Update all UI from _errorData (view is now guaranteed to be loaded) +#if !TARGET_OS_OSX // [macOS] _headerTitleLabel.text = _errorData.isCompileError ? @"Failed to compile" : @"Error"; [_stackTraceTableView reloadData]; [_stackTraceTableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0] atScrollPosition:UITableViewScrollPositionTop animated:NO]; +#else // [macOS + _headerTitleLabel.stringValue = _errorData.isCompileError ? @"Failed to compile" : @"Error"; + [_stackTraceTableView reloadData]; + [_stackTraceTableView scrollRowToVisible:0]; +#endif // macOS] [self startAutoRetryIfApplicable]; [self _startHMRClient]; @@ -307,7 +582,13 @@ - (void)showErrorMessage:(NSString *)message - (void)dismiss { [self stopAutoRetry]; +#if !TARGET_OS_OSX // [macOS] [self dismissViewControllerAnimated:NO completion:nil]; +#else // [macOS + if (self.presentingViewController) { + [[RCTKeyWindow() contentViewController] dismissViewController:self]; + } +#endif // macOS] } - (void)reload @@ -367,7 +648,11 @@ - (void)stopAutoRetry [_autoRetryTimer invalidate]; _autoRetryTimer = nil; if (_reloadButton) { +#if !TARGET_OS_OSX // [macOS] [_reloadButton setTitle:_reloadBaseText forState:UIControlStateNormal]; +#else // [macOS + _reloadButton.attributedTitle = [self attributedButtonTitle:_reloadBaseText]; +#endif // macOS] } } @@ -385,7 +670,11 @@ - (void)autoRetryTick - (void)updateReloadButtonTitle { NSString *title = [NSString stringWithFormat:@"%@ (%lds)", _reloadBaseText, (long)_autoRetryCountdown]; +#if !TARGET_OS_OSX // [macOS] [_reloadButton setTitle:title forState:UIControlStateNormal]; +#else // [macOS + _reloadButton.attributedTitle = [self attributedButtonTitle:title]; +#endif // macOS] } - (void)copyStack @@ -405,10 +694,16 @@ - (void)copyStack [fullStackTrace appendFormat:@" %@\n", [self formatFrameSource:stackFrame]]; } } +#if !TARGET_OS_OSX // [macOS] #if !TARGET_OS_TV UIPasteboard *pb = [UIPasteboard generalPasteboard]; [pb setString:fullStackTrace]; #endif +#else // [macOS + NSPasteboard *pasteboard = [NSPasteboard generalPasteboard]; + [pasteboard clearContents]; + [pasteboard setString:fullStackTrace forType:NSPasteboardTypeString]; +#endif // macOS] } - (NSString *)formatFrameSource:(RCTJSStackFrame *)stackFrame @@ -472,6 +767,7 @@ - (NSString *)displayMessage #pragma mark - TableView DataSource & Delegate +#if !TARGET_OS_OSX // [macOS] - (NSInteger)numberOfSectionsInTableView:(__unused UITableView *)tableView { return [self visibleSectionCount]; @@ -736,9 +1032,262 @@ - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath } [tableView deselectRowAtIndexPath:indexPath animated:YES]; } +#else // [macOS + +// macOS AppKit NSTableView has no notion of sections, so the RedBox 2.0 sections +// (Message, optional Source/Code Frame, optional Call Stack) are flattened into a +// single list of rows, with lightweight header rows for the Source/Call Stack groups. +typedef NS_ENUM(NSInteger, RCTRedBox2MacRowKind) { + RCTRedBox2MacRowKindMessage, + RCTRedBox2MacRowKindSourceHeader, + RCTRedBox2MacRowKindCodeFrame, + RCTRedBox2MacRowKindCallStackHeader, + RCTRedBox2MacRowKindStackFrame, +}; + +- (RCTRedBox2MacRowKind)macRowKindForRow:(NSInteger)row stackIndex:(NSInteger *)outStackIndex +{ + if (outStackIndex != nullptr) { + *outStackIndex = 0; + } + NSInteger current = 0; + if (_sectionStates[static_cast(Section::Message)].visible) { + if (row == current) { + return RCTRedBox2MacRowKindMessage; + } + current++; + } + if (_sectionStates[static_cast(Section::CodeFrame)].visible) { + if (row == current) { + return RCTRedBox2MacRowKindSourceHeader; + } + current++; + if (row == current) { + return RCTRedBox2MacRowKindCodeFrame; + } + current++; + } + if (_sectionStates[static_cast(Section::CallStack)].visible) { + if (row == current) { + return RCTRedBox2MacRowKindCallStackHeader; + } + current++; + if (outStackIndex != nullptr) { + *outStackIndex = row - current; + } + return RCTRedBox2MacRowKindStackFrame; + } + return RCTRedBox2MacRowKindMessage; +} + +- (NSInteger)numberOfRowsInTableView:(__unused NSTableView *)tableView +{ + NSInteger count = 0; + if (_sectionStates[static_cast(Section::Message)].visible) { + count += 1; + } + if (_sectionStates[static_cast(Section::CodeFrame)].visible) { + count += 2; // "Source" header + code frame + } + if (_sectionStates[static_cast(Section::CallStack)].visible) { + count += 1 + static_cast(_lastStackTrace.count); // "Call Stack" header + frames + } + return count; +} + +- (nullable NSView *)tableView:(__unused NSTableView *)tableView + viewForTableColumn:(nullable __unused NSTableColumn *)tableColumn + row:(NSInteger)row +{ + NSInteger stackIndex = 0; + switch ([self macRowKindForRow:row stackIndex:&stackIndex]) { + case RCTRedBox2MacRowKindMessage: + return [self macMessageCell]; + case RCTRedBox2MacRowKindSourceHeader: + return [self macHeaderCellWithTitle:@"Source"]; + case RCTRedBox2MacRowKindCodeFrame: + return [self macCodeFrameCell:_errorData]; + case RCTRedBox2MacRowKindCallStackHeader: + return [self macHeaderCellWithTitle:@"Call Stack"]; + case RCTRedBox2MacRowKindStackFrame: + return [self macStackFrameCell:_lastStackTrace[stackIndex]]; + } + return nil; +} + +- (NSView *)macMessageCell +{ + NSView *cell = [[NSView alloc] initWithFrame:NSZeroRect]; + cell.wantsLayer = YES; + cell.layer.backgroundColor = RCTRedBox2BackgroundColor().CGColor; + + NSTextField *categoryLabel = [self makeLabel]; + categoryLabel.textColor = RCTRedBox2ErrorColor(); + categoryLabel.font = [NSFont systemFontOfSize:21 weight:NSFontWeightBold]; + categoryLabel.maximumNumberOfLines = 1; + categoryLabel.stringValue = _errorData.title ?: @""; + [cell addSubview:categoryLabel]; + + NSTextField *messageLabel = [self makeLabel]; + messageLabel.accessibilityIdentifier = @"redbox-error"; + messageLabel.textColor = [NSColor whiteColor]; + messageLabel.font = [NSFont systemFontOfSize:14 weight:NSFontWeightMedium]; + messageLabel.stringValue = [self displayMessage] ?: @""; + [cell addSubview:messageLabel]; + + [NSLayoutConstraint activateConstraints:@[ + [categoryLabel.topAnchor constraintEqualToAnchor:cell.topAnchor constant:15], + [categoryLabel.leadingAnchor constraintEqualToAnchor:cell.leadingAnchor constant:12], + [categoryLabel.trailingAnchor constraintEqualToAnchor:cell.trailingAnchor constant:-12], + + [messageLabel.topAnchor constraintEqualToAnchor:categoryLabel.bottomAnchor constant:10], + [messageLabel.leadingAnchor constraintEqualToAnchor:cell.leadingAnchor constant:12], + [messageLabel.trailingAnchor constraintEqualToAnchor:cell.trailingAnchor constant:-12], + [messageLabel.bottomAnchor constraintEqualToAnchor:cell.bottomAnchor constant:-15], + ]]; + + return cell; +} + +- (NSView *)macHeaderCellWithTitle:(NSString *)title +{ + NSView *cell = [[NSView alloc] initWithFrame:NSZeroRect]; + + NSTextField *label = [self makeLabel]; + label.textColor = [NSColor whiteColor]; + label.font = [NSFont systemFontOfSize:18 weight:NSFontWeightSemibold]; + label.maximumNumberOfLines = 1; + label.stringValue = title; + [cell addSubview:label]; + + [NSLayoutConstraint activateConstraints:@[ + [label.leadingAnchor constraintEqualToAnchor:cell.leadingAnchor constant:12], + [label.trailingAnchor constraintEqualToAnchor:cell.trailingAnchor constant:-12], + [label.topAnchor constraintEqualToAnchor:cell.topAnchor constant:8], + [label.bottomAnchor constraintEqualToAnchor:cell.bottomAnchor constant:-10], + ]]; + + return cell; +} + +- (NSView *)macCodeFrameCell:(RCTRedBox2ErrorData *)errorData +{ + NSView *cell = [[NSView alloc] initWithFrame:NSZeroRect]; + + NSView *container = [[NSView alloc] initWithFrame:NSZeroRect]; + container.translatesAutoresizingMaskIntoConstraints = NO; + container.wantsLayer = YES; + container.layer.backgroundColor = RCTRedBox2BackgroundColor().CGColor; + container.layer.cornerRadius = 3; + [cell addSubview:container]; + + // Render code frame with ANSI syntax highlighting + NSFont *codeFont = [NSFont fontWithName:@"Menlo-Regular" size:12]; + NSAttributedString *highlighted = [RCTRedBox2AnsiParser attributedStringFromAnsiText:errorData.codeFrame + baseFont:codeFont + baseColor:[NSColor whiteColor]]; + + NSTextField *codeLabel = [self makeLabel]; + codeLabel.attributedStringValue = highlighted; + codeLabel.lineBreakMode = NSLineBreakByClipping; + [container addSubview:codeLabel]; + + // File name label below the code frame + NSTextField *fileLabel = [self makeLabel]; + NSString *fileName = errorData.codeFrameFileName.lastPathComponent ?: errorData.codeFrameFileName; + if (errorData.codeFrameRow > 0) { + fileLabel.stringValue = [NSString + stringWithFormat:@"%@ (%ld:%ld)", fileName, (long)errorData.codeFrameRow, (long)errorData.codeFrameColumn + 1]; + } else if (fileName.length > 0) { + fileLabel.stringValue = fileName; + } + fileLabel.textColor = RCTRedBox2TextColor(0.5); + fileLabel.font = [NSFont fontWithName:@"Menlo-Regular" size:12]; + fileLabel.alignment = NSTextAlignmentCenter; + fileLabel.maximumNumberOfLines = 1; + [cell addSubview:fileLabel]; + + [NSLayoutConstraint activateConstraints:@[ + [container.topAnchor constraintEqualToAnchor:cell.topAnchor constant:5], + [container.leadingAnchor constraintEqualToAnchor:cell.leadingAnchor constant:10], + [container.trailingAnchor constraintEqualToAnchor:cell.trailingAnchor constant:-10], + + [codeLabel.topAnchor constraintEqualToAnchor:container.topAnchor constant:10], + [codeLabel.leadingAnchor constraintEqualToAnchor:container.leadingAnchor constant:10], + [codeLabel.trailingAnchor constraintEqualToAnchor:container.trailingAnchor constant:-10], + [codeLabel.bottomAnchor constraintEqualToAnchor:container.bottomAnchor constant:-10], + + [fileLabel.topAnchor constraintEqualToAnchor:container.bottomAnchor constant:10], + [fileLabel.leadingAnchor constraintEqualToAnchor:cell.leadingAnchor constant:10], + [fileLabel.trailingAnchor constraintEqualToAnchor:cell.trailingAnchor constant:-10], + [fileLabel.bottomAnchor constraintEqualToAnchor:cell.bottomAnchor constant:-10], + ]]; + + return cell; +} + +- (NSView *)macStackFrameCell:(RCTJSStackFrame *)stackFrame +{ + NSView *cell = [[NSView alloc] initWithFrame:NSZeroRect]; + + NSTextField *label = [self makeLabel]; + label.maximumNumberOfLines = 2; + [cell addSubview:label]; + + NSMutableParagraphStyle *textParagraphStyle = [NSMutableParagraphStyle new]; + textParagraphStyle.lineBreakMode = NSLineBreakByCharWrapping; + + NSDictionary *textAttributes = @{ + NSForegroundColorAttributeName : stackFrame.collapse ? RCTRedBox2TextColor(0.4) : [NSColor whiteColor], + NSFontAttributeName : [NSFont fontWithName:@"Menlo-Regular" size:14], + NSParagraphStyleAttributeName : textParagraphStyle, + }; + NSString *text = stackFrame.methodName ?: @"(unnamed method)"; + NSMutableAttributedString *title = [[NSMutableAttributedString alloc] initWithString:text attributes:textAttributes]; + + if (stackFrame.file != nullptr) { + label.maximumNumberOfLines = 3; + + NSMutableParagraphStyle *detailParagraphStyle = [NSMutableParagraphStyle new]; + detailParagraphStyle.lineBreakMode = NSLineBreakByTruncatingMiddle; + + NSDictionary *detailAttributes = @{ + NSForegroundColorAttributeName : stackFrame.collapse ? RCTRedBox2TextColor(0.3) : RCTRedBox2TextColor(0.8), + NSFontAttributeName : [NSFont systemFontOfSize:12 weight:NSFontWeightLight], + NSParagraphStyleAttributeName : detailParagraphStyle, + }; + NSAttributedString *detail = [[NSAttributedString alloc] initWithString:[self formatFrameSource:stackFrame] + attributes:detailAttributes]; + [title appendAttributedString:[[NSAttributedString alloc] initWithString:@"\n"]]; + [title appendAttributedString:detail]; + } + + label.attributedStringValue = title; + + [NSLayoutConstraint activateConstraints:@[ + [label.leadingAnchor constraintEqualToAnchor:cell.leadingAnchor constant:12], + [label.trailingAnchor constraintEqualToAnchor:cell.trailingAnchor constant:-12], + [label.topAnchor constraintEqualToAnchor:cell.topAnchor constant:8], + [label.bottomAnchor constraintEqualToAnchor:cell.bottomAnchor constant:-8], + ]]; + + return cell; +} + +- (BOOL)tableView:(__unused NSTableView *)tableView shouldSelectRow:(NSInteger)row +{ + NSInteger stackIndex = 0; + if ([self macRowKindForRow:row stackIndex:&stackIndex] == RCTRedBox2MacRowKindStackFrame) { + RCTJSStackFrame *stackFrame = _lastStackTrace[stackIndex]; + [_actionDelegate redBoxController:self openStackFrameInEditor:stackFrame]; + } + return NO; +} +#endif // macOS] #pragma mark - Key Commands +#if !TARGET_OS_OSX // [macOS] - (NSArray *)keyCommands { return @[ @@ -758,6 +1307,7 @@ - (BOOL)canBecomeFirstResponder { return YES; } +#endif // [macOS] @end From 37169aebcb521e0e013cc103af868f75270a58a2 Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Mon, 27 Jul 2026 03:39:54 -0700 Subject: [PATCH 3/4] fix(macos): enable RedBox 2 AppKit controller Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- packages/react-native/React/CoreModules/RCTRedBox.mm | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/react-native/React/CoreModules/RCTRedBox.mm b/packages/react-native/React/CoreModules/RCTRedBox.mm index 9f425218f10..6e0a2b617c2 100644 --- a/packages/react-native/React/CoreModules/RCTRedBox.mm +++ b/packages/react-native/React/CoreModules/RCTRedBox.mm @@ -21,9 +21,7 @@ #import "CoreModulesPlugins.h" #import "RCTRedBox+Internal.h" -#if !TARGET_OS_OSX // [macOS] #import "RCTRedBox2Controller+Internal.h" -#endif // [macOS] #import "RCTRedBoxController+Internal.h" #if RCT_DEV_MENU @@ -197,11 +195,9 @@ - (void)showErrorMessage:(NSString *)message errorInfo = [self _customizeError:errorInfo]; if (self->_controller == nullptr) { - if (!TARGET_OS_OSX && facebook::react::ReactNativeFeatureFlags::redBoxV2IOS()) { // [macOS] -#if !TARGET_OS_OSX // [macOS] + if (facebook::react::ReactNativeFeatureFlags::redBoxV2IOS()) { self->_controller = [[RCTRedBox2Controller alloc] initWithCustomButtonTitles:self->_customButtonTitles customButtonHandlers:self->_customButtonHandlers]; -#endif // [macOS] } else { self->_controller = [[RCTRedBoxController alloc] initWithCustomButtonTitles:self->_customButtonTitles customButtonHandlers:self->_customButtonHandlers]; From b7d428fe6af7297ebc63fec755fe7f405228fc0f Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Tue, 28 Jul 2026 21:09:08 -0700 Subject: [PATCH 4/4] refactor(macos): migrate RedBox 2.0 to RCTUIKit primitives Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../RCTRedBox2Controller+Internal.h | 8 +- .../React/CoreModules/RCTRedBox2Controller.mm | 790 ++++-------------- 2 files changed, 162 insertions(+), 636 deletions(-) diff --git a/packages/react-native/React/CoreModules/RCTRedBox2Controller+Internal.h b/packages/react-native/React/CoreModules/RCTRedBox2Controller+Internal.h index 7339d6dc010..905e9398487 100644 --- a/packages/react-native/React/CoreModules/RCTRedBox2Controller+Internal.h +++ b/packages/react-native/React/CoreModules/RCTRedBox2Controller+Internal.h @@ -6,6 +6,7 @@ */ #import +#import // [macOS] #import "RCTRedBox+Internal.h" @@ -13,11 +14,8 @@ typedef void (^RCTRedBox2ButtonPressHandler)(void); -#if !TARGET_OS_OSX // [macOS] -@interface RCTRedBox2Controller : UIViewController -#else // [macOS -@interface RCTRedBox2Controller : NSViewController -#endif // macOS] +@interface RCTRedBox2Controller + : RCTPlatformViewController // [macOS] @property (nonatomic, weak) id actionDelegate; diff --git a/packages/react-native/React/CoreModules/RCTRedBox2Controller.mm b/packages/react-native/React/CoreModules/RCTRedBox2Controller.mm index 1be4b3b7cc8..f17591f1e98 100644 --- a/packages/react-native/React/CoreModules/RCTRedBox2Controller.mm +++ b/packages/react-native/React/CoreModules/RCTRedBox2Controller.mm @@ -13,9 +13,6 @@ #import #include -#if TARGET_OS_OSX // [macOS -#import -#endif // macOS] #import "RCTJscSafeUrl+Internal.h" #import "RCTRedBox2AnsiParser+Internal.h" @@ -30,22 +27,7 @@ #pragma mark - RCTRedBox2Controller // Color Palette (matching LogBoxStyle.js) -#if !TARGET_OS_OSX // [macOS] -static UIColor *RCTRedBox2BackgroundColor() -{ - return [UIColor colorWithRed:51.0 / 255 green:51.0 / 255 blue:51.0 / 255 alpha:1.0]; -} - -static UIColor *RCTRedBox2ErrorColor() -{ - return [UIColor colorWithRed:243.0 / 255 green:83.0 / 255 blue:105.0 / 255 alpha:1.0]; -} - -static UIColor *RCTRedBox2TextColor(CGFloat opacity) -{ - return [UIColor colorWithWhite:1.0 alpha:opacity]; -} -#else // [macOS +// [macOS static RCTUIColor *RCTRedBox2BackgroundColor() { return [RCTUIColor colorWithRed:51.0 / 255 green:51.0 / 255 blue:51.0 / 255 alpha:1.0]; @@ -60,7 +42,7 @@ { return [RCTUIColor colorWithWhite:1.0 alpha:opacity]; } -#endif // macOS] +// macOS] enum class Section : uint8_t { Message, CodeFrame, CallStack, kMaxValue }; static constexpr size_t kSectionCount = static_cast(Section::kMaxValue); @@ -70,51 +52,13 @@ }; static const NSTimeInterval kAutoRetryInterval = 20.0; -#if TARGET_OS_OSX // [macOS -// AppKit has no per-control block-based action, so associate the handler with the button. -@interface NSButton (RCTRedBox2) -@property (nonatomic) RCTRedBox2ButtonPressHandler rb2_handler; -- (void)rb2_addBlock:(RCTRedBox2ButtonPressHandler)handler; -@end - -@implementation NSButton (RCTRedBox2) - -- (RCTRedBox2ButtonPressHandler)rb2_handler -{ - return objc_getAssociatedObject(self, @selector(rb2_handler)); -} - -- (void)setRb2_handler:(RCTRedBox2ButtonPressHandler)rb2_handler -{ - objc_setAssociatedObject(self, @selector(rb2_handler), rb2_handler, OBJC_ASSOCIATION_RETAIN_NONATOMIC); -} - -- (void)rb2_callBlock -{ - if (self.rb2_handler) { - self.rb2_handler(); - } -} - -- (void)rb2_addBlock:(RCTRedBox2ButtonPressHandler)handler -{ - self.rb2_handler = handler; - [self setTarget:self]; - [self setAction:@selector(rb2_callBlock)]; -} - -@end -#endif // macOS] @implementation RCTRedBox2Controller { -#if !TARGET_OS_OSX // [macOS] - UITableView *_stackTraceTableView; - UILabel *_headerTitleLabel; - UILabel *_errorCategoryLabel; -#else // [macOS - NSTableView *_stackTraceTableView; - NSTextField *_headerTitleLabel; -#endif // macOS] + // [macOS + RCTUITableView *_stackTraceTableView; + RCTUILabel *_headerTitleLabel; + RCTUILabel *_errorCategoryLabel; + // macOS] NSString *_lastErrorMessage; NSArray *_lastStackTrace; NSArray *_customButtonTitles; @@ -124,11 +68,7 @@ @implementation RCTRedBox2Controller { std::array _sectionStates; NSTimer *_autoRetryTimer; NSInteger _autoRetryCountdown; -#if !TARGET_OS_OSX // [macOS] - UIButton *_reloadButton; -#else // [macOS - NSButton *_reloadButton; -#endif // macOS] + RCTUIButton *_reloadButton; // [macOS] NSString *_reloadBaseText; RCTRedBoxHMRClient *_hmrClient; } @@ -158,32 +98,25 @@ - (void)viewDidLoad self.view.layer.backgroundColor = RCTRedBox2BackgroundColor().CGColor; #endif // macOS] - // Header bar (adds itself to self.view) -#if !TARGET_OS_OSX // [macOS] - UIView *headerBar = [self createHeaderBar]; -#else // [macOS - RCTPlatformView *headerBar = [self createHeaderBar]; -#endif // macOS] - - // Footer button bar -#if !TARGET_OS_OSX // [macOS] - UIView *footerBar = [self createFooterBar]; -#else // [macOS - RCTPlatformView *footerBar = [self createFooterBar]; -#endif // macOS] + RCTPlatformView *headerBar = [self createHeaderBar]; // [macOS] + RCTPlatformView *footerBar = [self createFooterBar]; // [macOS] // Stack trace table #if !TARGET_OS_OSX // [macOS] - _stackTraceTableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain]; - _stackTraceTableView.translatesAutoresizingMaskIntoConstraints = NO; - _stackTraceTableView.delegate = self; - _stackTraceTableView.dataSource = self; + _stackTraceTableView = [[RCTUITableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain]; // [macOS] _stackTraceTableView.backgroundColor = [UIColor clearColor]; #if !TARGET_OS_TV _stackTraceTableView.separatorStyle = UITableViewCellSeparatorStyleNone; #endif _stackTraceTableView.indicatorStyle = UIScrollViewIndicatorStyleWhite; _stackTraceTableView.bounces = NO; +#else // [macOS + _stackTraceTableView = [[RCTUITableView alloc] initWithFrame:NSZeroRect]; + _stackTraceTableView.hasVerticalScroller = YES; +#endif // macOS] + _stackTraceTableView.translatesAutoresizingMaskIntoConstraints = NO; + _stackTraceTableView.dataSource = self; + _stackTraceTableView.delegate = self; [self.view addSubview:_stackTraceTableView]; [NSLayoutConstraint activateConstraints:@[ @@ -192,319 +125,149 @@ - (void)viewDidLoad [_stackTraceTableView.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor], [_stackTraceTableView.bottomAnchor constraintEqualToAnchor:footerBar.topAnchor], ]]; -#else // [macOS - NSScrollView *scrollView = [[NSScrollView alloc] initWithFrame:NSZeroRect]; - scrollView.translatesAutoresizingMaskIntoConstraints = NO; - scrollView.drawsBackground = NO; - scrollView.hasVerticalScroller = YES; - - _stackTraceTableView = [[NSTableView alloc] initWithFrame:NSZeroRect]; - _stackTraceTableView.translatesAutoresizingMaskIntoConstraints = NO; - _stackTraceTableView.dataSource = self; - _stackTraceTableView.delegate = self; - _stackTraceTableView.headerView = nil; - _stackTraceTableView.backgroundColor = [NSColor clearColor]; - _stackTraceTableView.selectionHighlightStyle = NSTableViewSelectionHighlightStyleNone; - _stackTraceTableView.usesAutomaticRowHeights = YES; - _stackTraceTableView.intercellSpacing = NSMakeSize(0, 0); - _stackTraceTableView.allowsColumnReordering = NO; - _stackTraceTableView.allowsColumnResizing = NO; - _stackTraceTableView.columnAutoresizingStyle = NSTableViewFirstColumnOnlyAutoresizingStyle; - - NSTableColumn *tableColumn = [[NSTableColumn alloc] initWithIdentifier:@"info"]; - [_stackTraceTableView addTableColumn:tableColumn]; - - scrollView.documentView = _stackTraceTableView; - [self.view addSubview:scrollView]; - - [NSLayoutConstraint activateConstraints:@[ - [scrollView.topAnchor constraintEqualToAnchor:headerBar.bottomAnchor], - [scrollView.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor], - [scrollView.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor], - [scrollView.bottomAnchor constraintEqualToAnchor:footerBar.topAnchor], - ]]; -#endif // macOS] } #pragma mark - Header Bar -#if !TARGET_OS_OSX // [macOS] -- (UIView *)createHeaderBar +- (RCTUILabel *)makeLabel // [macOS] { - UIView *headerContainer = [[UIView alloc] init]; + RCTUILabel *label = [[RCTUILabel alloc] initWithFrame:CGRectZero]; + label.translatesAutoresizingMaskIntoConstraints = NO; + label.lineBreakMode = NSLineBreakByWordWrapping; + label.numberOfLines = 0; // [macOS] + return label; +} + +- (RCTPlatformView *)createHeaderBar // [macOS] +{ + RCTUIView *headerContainer = [[RCTUIView alloc] init]; // [macOS] headerContainer.translatesAutoresizingMaskIntoConstraints = NO; headerContainer.backgroundColor = RCTRedBox2ErrorColor(); - _headerTitleLabel = [[UILabel alloc] init]; - _headerTitleLabel.translatesAutoresizingMaskIntoConstraints = NO; - _headerTitleLabel.textColor = [UIColor whiteColor]; - _headerTitleLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightSemibold]; - _headerTitleLabel.textAlignment = NSTextAlignmentCenter; + _headerTitleLabel = [self makeLabel]; // [macOS] + _headerTitleLabel.textColor = [RCTUIColor whiteColor]; // [macOS] + _headerTitleLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightSemibold]; // [macOS] + _headerTitleLabel.textAlignment = NSTextAlignmentCenter; // [macOS] [headerContainer addSubview:_headerTitleLabel]; - [self.view addSubview:headerContainer]; [NSLayoutConstraint activateConstraints:@[ [headerContainer.topAnchor constraintEqualToAnchor:self.view.topAnchor], [headerContainer.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor], [headerContainer.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor], - [_headerTitleLabel.leadingAnchor constraintEqualToAnchor:headerContainer.leadingAnchor constant:12], [_headerTitleLabel.trailingAnchor constraintEqualToAnchor:headerContainer.trailingAnchor constant:-12], [_headerTitleLabel.bottomAnchor constraintEqualToAnchor:headerContainer.bottomAnchor constant:-12], +#if !TARGET_OS_OSX // [macOS] [_headerTitleLabel.topAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.topAnchor constant:12], - ]]; - - return headerContainer; -} #else // [macOS -- (RCTPlatformView *)createHeaderBar -{ - NSView *headerContainer = [[NSView alloc] init]; - headerContainer.translatesAutoresizingMaskIntoConstraints = NO; - headerContainer.wantsLayer = YES; - headerContainer.layer.backgroundColor = RCTRedBox2ErrorColor().CGColor; - - _headerTitleLabel = [self makeLabel]; - _headerTitleLabel.textColor = [NSColor whiteColor]; - _headerTitleLabel.font = [NSFont systemFontOfSize:16 weight:NSFontWeightSemibold]; - _headerTitleLabel.alignment = NSTextAlignmentCenter; - [headerContainer addSubview:_headerTitleLabel]; - - [self.view addSubview:headerContainer]; - - [NSLayoutConstraint activateConstraints:@[ - [headerContainer.topAnchor constraintEqualToAnchor:self.view.topAnchor], - [headerContainer.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor], - [headerContainer.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor], - - [_headerTitleLabel.leadingAnchor constraintEqualToAnchor:headerContainer.leadingAnchor constant:12], - [_headerTitleLabel.trailingAnchor constraintEqualToAnchor:headerContainer.trailingAnchor constant:-12], - [_headerTitleLabel.bottomAnchor constraintEqualToAnchor:headerContainer.bottomAnchor constant:-12], [_headerTitleLabel.topAnchor constraintEqualToAnchor:headerContainer.topAnchor constant:12], +#endif // macOS] ]]; return headerContainer; } -#endif // macOS] #pragma mark - Footer Bar -#if !TARGET_OS_OSX // [macOS] -- (UIView *)createFooterBar +- (RCTPlatformView *)createFooterBar // [macOS] { const CGFloat buttonHeight = 48; - NSString *reloadText = @"Reload"; - NSString *dismissText = @"Dismiss"; - NSString *copyText = @"Copy"; - UIButton *dismissButton = [self footerButton:dismissText - accessibilityIdentifier:@"redbox-dismiss" - selector:@selector(dismiss)]; + __weak __typeof(self) weakSelf = self; + RCTUIButton *dismissButton = [self footerButton:@"Dismiss" + accessibilityIdentifier:@"redbox-dismiss" + handler:^{ + [weakSelf dismiss]; + }]; // [macOS] _reloadBaseText = reloadText; - _reloadButton = [self footerButton:reloadText accessibilityIdentifier:@"redbox-reload" selector:@selector(reload)]; - UIButton *copyButton = [self footerButton:copyText - accessibilityIdentifier:@"redbox-copy" - selector:@selector(copyStack)]; + _reloadButton = [self footerButton:reloadText + accessibilityIdentifier:@"redbox-reload" + handler:^{ + [weakSelf reload]; + }]; + RCTUIButton *copyButton = [self footerButton:@"Copy" + accessibilityIdentifier:@"redbox-copy" + handler:^{ + [weakSelf copyStack]; + }]; // [macOS] +#if !TARGET_OS_OSX // [macOS] UIStackView *buttonStackView = [[UIStackView alloc] init]; - buttonStackView.translatesAutoresizingMaskIntoConstraints = NO; buttonStackView.axis = UILayoutConstraintAxisHorizontal; buttonStackView.distribution = UIStackViewDistributionFillEqually; buttonStackView.alignment = UIStackViewAlignmentTop; buttonStackView.backgroundColor = RCTRedBox2BackgroundColor(); - - [buttonStackView addArrangedSubview:dismissButton]; - [buttonStackView addArrangedSubview:_reloadButton]; - [buttonStackView addArrangedSubview:copyButton]; - - for (NSUInteger i = 0; i < [_customButtonTitles count]; i++) { - UIButton *button = [self footerButton:_customButtonTitles[i] - accessibilityIdentifier:@"" - handler:_customButtonHandlers[i]]; - [buttonStackView addArrangedSubview:button]; - } - - // Shadow layer above footer - buttonStackView.layer.shadowColor = [UIColor blackColor].CGColor; - buttonStackView.layer.shadowOffset = CGSizeMake(0, -2); - buttonStackView.layer.shadowRadius = 2; - buttonStackView.layer.shadowOpacity = 0.5; - - [self.view addSubview:buttonStackView]; - - CGFloat bottomInset = [self bottomSafeViewHeight]; - - [NSLayoutConstraint activateConstraints:@[ - [buttonStackView.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor], - [buttonStackView.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor], - [buttonStackView.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor], - [buttonStackView.heightAnchor constraintEqualToConstant:buttonHeight + bottomInset], - ]]; - - for (UIButton *btn in buttonStackView.arrangedSubviews) { - [btn.heightAnchor constraintEqualToConstant:buttonHeight].active = YES; - } - - return buttonStackView; -} #else // [macOS -- (RCTPlatformView *)createFooterBar -{ - const CGFloat buttonHeight = 48; - - NSString *reloadText = @"Reload"; - NSString *dismissText = @"Dismiss"; - NSString *copyText = @"Copy"; - - NSButton *dismissButton = [self footerButton:dismissText - accessibilityIdentifier:@"redbox-dismiss" - selector:@selector(dismiss)]; [dismissButton setKeyEquivalent:@"\e"]; - _reloadBaseText = reloadText; - _reloadButton = [self footerButton:reloadText accessibilityIdentifier:@"redbox-reload" selector:@selector(reload)]; [_reloadButton setKeyEquivalent:@"r"]; [_reloadButton setKeyEquivalentModifierMask:NSEventModifierFlagCommand]; - NSButton *copyButton = [self footerButton:copyText - accessibilityIdentifier:@"redbox-copy" - selector:@selector(copyStack)]; [copyButton setKeyEquivalent:@"c"]; [copyButton setKeyEquivalentModifierMask:NSEventModifierFlagOption | NSEventModifierFlagCommand]; NSStackView *buttonStackView = [[NSStackView alloc] init]; - buttonStackView.translatesAutoresizingMaskIntoConstraints = NO; buttonStackView.orientation = NSUserInterfaceLayoutOrientationHorizontal; buttonStackView.distribution = NSStackViewDistributionFillEqually; buttonStackView.alignment = NSLayoutAttributeCenterY; buttonStackView.wantsLayer = YES; buttonStackView.layer.backgroundColor = RCTRedBox2BackgroundColor().CGColor; - +#endif // macOS] + buttonStackView.translatesAutoresizingMaskIntoConstraints = NO; [buttonStackView addArrangedSubview:dismissButton]; [buttonStackView addArrangedSubview:_reloadButton]; [buttonStackView addArrangedSubview:copyButton]; for (NSUInteger i = 0; i < [_customButtonTitles count]; i++) { - NSButton *button = [self footerButton:_customButtonTitles[i] - accessibilityIdentifier:@"" - handler:_customButtonHandlers[i]]; + RCTUIButton *button = [self footerButton:_customButtonTitles[i] + accessibilityIdentifier:@"" + handler:_customButtonHandlers[i]]; // [macOS] [buttonStackView addArrangedSubview:button]; } - // Shadow layer above footer - buttonStackView.layer.shadowColor = [NSColor blackColor].CGColor; + buttonStackView.layer.shadowColor = [RCTUIColor blackColor].CGColor; // [macOS] buttonStackView.layer.shadowOffset = CGSizeMake(0, -2); buttonStackView.layer.shadowRadius = 2; buttonStackView.layer.shadowOpacity = 0.5; - [self.view addSubview:buttonStackView]; - CGFloat bottomInset = [self bottomSafeViewHeight]; - [NSLayoutConstraint activateConstraints:@[ [buttonStackView.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor], [buttonStackView.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor], [buttonStackView.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor], - [buttonStackView.heightAnchor constraintEqualToConstant:buttonHeight + bottomInset], + [buttonStackView.heightAnchor constraintEqualToConstant:buttonHeight + [self bottomSafeViewHeight]], ]]; + for (RCTPlatformView *button in buttonStackView.arrangedSubviews) { // [macOS] + [button.heightAnchor constraintEqualToConstant:buttonHeight].active = YES; + } + return buttonStackView; } -#endif // macOS] -#if !TARGET_OS_OSX // [macOS] -- (UIButton *)styledButton:(NSString *)title accessibilityIdentifier:(NSString *)accessibilityIdentifier +- (RCTUIButton *)styledButton:(NSString *)title accessibilityIdentifier:(NSString *)accessibilityIdentifier // [macOS] { - UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom]; + RCTUIButton *button = [[RCTUIButton alloc] initWithFrame:CGRectZero]; // [macOS] + button.translatesAutoresizingMaskIntoConstraints = NO; button.accessibilityIdentifier = accessibilityIdentifier; button.titleLabel.font = [UIFont systemFontOfSize:14]; button.titleLabel.textAlignment = NSTextAlignmentCenter; button.backgroundColor = RCTRedBox2BackgroundColor(); - [button setTitle:title forState:UIControlStateNormal]; - [button setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; - [button setTitleColor:RCTRedBox2TextColor(0.5) forState:UIControlStateHighlighted]; - return button; -} - -- (UIButton *)footerButton:(NSString *)title - accessibilityIdentifier:(NSString *)accessibilityIdentifier - selector:(SEL)selector -{ - UIButton *button = [self styledButton:title accessibilityIdentifier:accessibilityIdentifier]; - [button addTarget:self action:selector forControlEvents:UIControlEventTouchUpInside]; - return button; -} - -- (UIButton *)footerButton:(NSString *)title - accessibilityIdentifier:(NSString *)accessibilityIdentifier - handler:(RCTRedBox2ButtonPressHandler)handler -{ - UIButton *button = [self styledButton:title accessibilityIdentifier:accessibilityIdentifier]; - [button addAction:[UIAction actionWithHandler:^(__unused UIAction *action) { - handler(); - }] - forControlEvents:UIControlEventTouchUpInside]; - return button; -} -#else // [macOS -- (NSTextField *)makeLabel -{ - NSTextField *label = [[NSTextField alloc] initWithFrame:NSZeroRect]; - label.translatesAutoresizingMaskIntoConstraints = NO; - label.drawsBackground = NO; - label.bezeled = NO; - label.editable = NO; - label.selectable = NO; - label.lineBreakMode = NSLineBreakByWordWrapping; - label.maximumNumberOfLines = 0; - return label; -} - -- (NSAttributedString *)attributedButtonTitle:(NSString *)title -{ - NSMutableParagraphStyle *paragraphStyle = [NSMutableParagraphStyle new]; - paragraphStyle.alignment = NSTextAlignmentCenter; - return [[NSAttributedString alloc] initWithString:title - attributes:@{ - NSForegroundColorAttributeName : [NSColor whiteColor], - NSFontAttributeName : [NSFont systemFontOfSize:14], - NSParagraphStyleAttributeName : paragraphStyle, - }]; -} - -- (NSButton *)styledButton:(NSString *)title accessibilityIdentifier:(NSString *)accessibilityIdentifier -{ - NSButton *button = [[NSButton alloc] initWithFrame:NSZeroRect]; - button.translatesAutoresizingMaskIntoConstraints = NO; - button.accessibilityIdentifier = accessibilityIdentifier; - button.bordered = NO; - button.wantsLayer = YES; - button.layer.backgroundColor = RCTRedBox2BackgroundColor().CGColor; - [button setButtonType:NSButtonTypeMomentaryPushIn]; - button.attributedTitle = [self attributedButtonTitle:title]; + [button setTitle:title forState:RCTUIControlStateNormal]; // [macOS] + [button setTitleColor:[RCTUIColor whiteColor] forState:RCTUIControlStateNormal]; // [macOS] + [button setTitleColor:RCTRedBox2TextColor(0.5) forState:RCTUIControlStateHighlighted]; // [macOS] return button; } -- (NSButton *)footerButton:(NSString *)title - accessibilityIdentifier:(NSString *)accessibilityIdentifier - selector:(SEL)selector +- (RCTUIButton *)footerButton:(NSString *)title // [macOS] + accessibilityIdentifier:(NSString *)accessibilityIdentifier + handler:(RCTRedBox2ButtonPressHandler)handler { - NSButton *button = [self styledButton:title accessibilityIdentifier:accessibilityIdentifier]; - button.target = self; - button.action = selector; + RCTUIButton *button = [self styledButton:title accessibilityIdentifier:accessibilityIdentifier]; // [macOS] + [button rct_setPrimaryAction:[RCTUIAction actionWithHandler:handler]]; // [macOS] return button; } -- (NSButton *)footerButton:(NSString *)title - accessibilityIdentifier:(NSString *)accessibilityIdentifier - handler:(RCTRedBox2ButtonPressHandler)handler -{ - NSButton *button = [self styledButton:title accessibilityIdentifier:accessibilityIdentifier]; - [button rb2_addBlock:handler]; - return button; -} -#endif // macOS] - - (CGFloat)bottomSafeViewHeight { #if TARGET_OS_MACCATALYST || TARGET_OS_OSX // [macOS] @@ -562,17 +325,11 @@ - (void)showErrorMessage:(NSString *)message } // Update all UI from _errorData (view is now guaranteed to be loaded) -#if !TARGET_OS_OSX // [macOS] - _headerTitleLabel.text = _errorData.isCompileError ? @"Failed to compile" : @"Error"; + _headerTitleLabel.text = _errorData.isCompileError ? @"Failed to compile" : @"Error"; // [macOS] [_stackTraceTableView reloadData]; [_stackTraceTableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0] - atScrollPosition:UITableViewScrollPositionTop - animated:NO]; -#else // [macOS - _headerTitleLabel.stringValue = _errorData.isCompileError ? @"Failed to compile" : @"Error"; - [_stackTraceTableView reloadData]; - [_stackTraceTableView scrollRowToVisible:0]; -#endif // macOS] + atScrollPosition:RCTUITableViewScrollPositionTop + animated:NO]; // [macOS] [self startAutoRetryIfApplicable]; [self _startHMRClient]; @@ -648,11 +405,7 @@ - (void)stopAutoRetry [_autoRetryTimer invalidate]; _autoRetryTimer = nil; if (_reloadButton) { -#if !TARGET_OS_OSX // [macOS] - [_reloadButton setTitle:_reloadBaseText forState:UIControlStateNormal]; -#else // [macOS - _reloadButton.attributedTitle = [self attributedButtonTitle:_reloadBaseText]; -#endif // macOS] + [_reloadButton setTitle:_reloadBaseText forState:RCTUIControlStateNormal]; // [macOS] } } @@ -670,11 +423,7 @@ - (void)autoRetryTick - (void)updateReloadButtonTitle { NSString *title = [NSString stringWithFormat:@"%@ (%lds)", _reloadBaseText, (long)_autoRetryCountdown]; -#if !TARGET_OS_OSX // [macOS] - [_reloadButton setTitle:title forState:UIControlStateNormal]; -#else // [macOS - _reloadButton.attributedTitle = [self attributedButtonTitle:title]; -#endif // macOS] + [_reloadButton setTitle:title forState:RCTUIControlStateNormal]; // [macOS] } - (void)copyStack @@ -767,13 +516,12 @@ - (NSString *)displayMessage #pragma mark - TableView DataSource & Delegate -#if !TARGET_OS_OSX // [macOS] -- (NSInteger)numberOfSectionsInTableView:(__unused UITableView *)tableView +- (NSInteger)numberOfSectionsInTableView:(__unused RCTUITableView *)tableView // [macOS] { return [self visibleSectionCount]; } -- (NSInteger)tableView:(__unused UITableView *)tableView numberOfRowsInSection:(NSInteger)section +- (NSInteger)tableView:(__unused RCTUITableView *)tableView numberOfRowsInSection:(NSInteger)section // [macOS] { if ([self sectionForIndex:section] == Section::CallStack) { return static_cast(_lastStackTrace.count); @@ -781,49 +529,57 @@ - (NSInteger)tableView:(__unused UITableView *)tableView numberOfRowsInSection:( return 1; } -- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath +- (RCTUITableViewCell *)tableView:(RCTUITableView *)tableView // [macOS] + cellForRowAtIndexPath:(NSIndexPath *)indexPath { switch ([self sectionForIndex:indexPath.section]) { case Section::Message: { - UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"msg-cell"]; + RCTUITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"msg-cell"]; // [macOS] return [self reuseCell:cell forErrorMessage:[self displayMessage]]; } case Section::CodeFrame: { - UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"code-cell"]; + RCTUITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"code-cell"]; // [macOS] return [self reuseCell:cell forCodeFrame:_errorData]; } case Section::CallStack: case Section::kMaxValue: break; } - UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"]; + RCTUITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"]; // [macOS] NSUInteger index = indexPath.row; RCTJSStackFrame *stackFrame = _lastStackTrace[index]; return [self reuseCell:cell forStackFrame:stackFrame]; } -- (UITableViewCell *)reuseCell:(UITableViewCell *)cell forErrorMessage:(NSString *)message +- (RCTUITableViewCell *)reuseCell:(RCTUITableViewCell *)cell forErrorMessage:(NSString *)message // [macOS] { if (cell == nullptr) { - cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"msg-cell"]; + cell = [[RCTUITableViewCell alloc] initWithStyle:RCTUITableViewCellStyleDefault + reuseIdentifier:@"msg-cell"]; // [macOS] + cell.textLabel.hidden = YES; +#if !TARGET_OS_OSX // [macOS] cell.backgroundColor = RCTRedBox2BackgroundColor(); cell.selectionStyle = UITableViewCellSelectionStyleNone; +#else // [macOS + cell.wantsLayer = YES; + cell.layer.backgroundColor = RCTRedBox2BackgroundColor().CGColor; + cell.layer.cornerRadius = 8.0; + cell.layer.cornerCurve = kCACornerCurveContinuous; +#endif // macOS] // Error category label (e.g. "Syntax Error", "Uncaught Error") - _errorCategoryLabel = [[UILabel alloc] init]; - _errorCategoryLabel.translatesAutoresizingMaskIntoConstraints = NO; + _errorCategoryLabel = [self makeLabel]; // [macOS] + _errorCategoryLabel.tag = 101; _errorCategoryLabel.textColor = RCTRedBox2ErrorColor(); _errorCategoryLabel.font = [UIFont systemFontOfSize:21 weight:UIFontWeightBold]; _errorCategoryLabel.numberOfLines = 1; [cell.contentView addSubview:_errorCategoryLabel]; // Error message label - UILabel *messageLabel = [[UILabel alloc] init]; - messageLabel.translatesAutoresizingMaskIntoConstraints = NO; + RCTUILabel *messageLabel = [self makeLabel]; // [macOS] messageLabel.accessibilityIdentifier = @"redbox-error"; - messageLabel.textColor = [UIColor whiteColor]; + messageLabel.textColor = [RCTUIColor whiteColor]; // [macOS] messageLabel.font = [UIFont systemFontOfSize:14 weight:UIFontWeightMedium]; - messageLabel.lineBreakMode = NSLineBreakByWordWrapping; messageLabel.numberOfLines = 0; messageLabel.tag = 100; [cell.contentView addSubview:messageLabel]; @@ -840,26 +596,30 @@ - (UITableViewCell *)reuseCell:(UITableViewCell *)cell forErrorMessage:(NSString ]]; } + _errorCategoryLabel = [cell.contentView viewWithTag:101]; _errorCategoryLabel.text = _errorData.title; - UILabel *messageLabel = [cell.contentView viewWithTag:100]; + RCTUILabel *messageLabel = [cell.contentView viewWithTag:100]; // [macOS] messageLabel.text = message; return cell; } -- (UITableViewCell *)reuseCell:(UITableViewCell *)cell forStackFrame:(RCTJSStackFrame *)stackFrame +- (RCTUITableViewCell *)reuseCell:(RCTUITableViewCell *)cell forStackFrame:(RCTJSStackFrame *)stackFrame // [macOS] { if (cell == nullptr) { - cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:@"cell"]; + cell = [[RCTUITableViewCell alloc] initWithStyle:RCTUITableViewCellStyleSubtitle + reuseIdentifier:@"cell"]; // [macOS] cell.textLabel.font = [UIFont fontWithName:@"Menlo-Regular" size:14]; cell.textLabel.lineBreakMode = NSLineBreakByCharWrapping; cell.textLabel.numberOfLines = 2; cell.detailTextLabel.font = [UIFont systemFontOfSize:12 weight:UIFontWeightLight]; cell.detailTextLabel.lineBreakMode = NSLineBreakByTruncatingMiddle; - cell.backgroundColor = [UIColor clearColor]; + cell.backgroundColor = [RCTUIColor clearColor]; // [macOS] +#if !TARGET_OS_OSX // [macOS] cell.selectedBackgroundView = [UIView new]; cell.selectedBackgroundView.backgroundColor = RCTRedBox2BackgroundColor(); cell.selectedBackgroundView.layer.cornerRadius = 5; +#endif // [macOS] } cell.textLabel.text = stackFrame.methodName ?: @"(unnamed method)"; @@ -873,46 +633,56 @@ - (UITableViewCell *)reuseCell:(UITableViewCell *)cell forStackFrame:(RCTJSStack cell.textLabel.textColor = RCTRedBox2TextColor(0.4); cell.detailTextLabel.textColor = RCTRedBox2TextColor(0.3); } else { - cell.textLabel.textColor = [UIColor whiteColor]; + cell.textLabel.textColor = [RCTUIColor whiteColor]; // [macOS] cell.detailTextLabel.textColor = RCTRedBox2TextColor(0.8); } return cell; } -- (UITableViewCell *)reuseCell:(UITableViewCell *)cell forCodeFrame:(RCTRedBox2ErrorData *)errorData +- (RCTUITableViewCell *)reuseCell:(RCTUITableViewCell *)cell forCodeFrame:(RCTRedBox2ErrorData *)errorData // [macOS] { if (cell == nullptr) { - cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"code-cell"]; - cell.backgroundColor = [UIColor clearColor]; + cell = [[RCTUITableViewCell alloc] initWithStyle:RCTUITableViewCellStyleDefault + reuseIdentifier:@"code-cell"]; // [macOS] + cell.textLabel.hidden = YES; + cell.backgroundColor = [RCTUIColor clearColor]; // [macOS] +#if !TARGET_OS_OSX // [macOS] cell.selectionStyle = UITableViewCellSelectionStyleNone; +#endif // [macOS] } // Remove old subviews - for (UIView *subview in cell.contentView.subviews) { - [subview removeFromSuperview]; + for (RCTPlatformView *subview in cell.contentView.subviews) { // [macOS] + if (subview != cell.textLabel && subview != cell.detailTextLabel) { + [subview removeFromSuperview]; + } } // Code frame container with rounded corners - UIView *container = [[UIView alloc] init]; + RCTUIView *container = [[RCTUIView alloc] init]; // [macOS] container.translatesAutoresizingMaskIntoConstraints = NO; container.backgroundColor = RCTRedBox2BackgroundColor(); container.layer.cornerRadius = 3; - container.clipsToBounds = YES; + container.layer.masksToBounds = YES; [cell.contentView addSubview:container]; // Render code frame with ANSI syntax highlighting UIFont *codeFont = [UIFont fontWithName:@"Menlo-Regular" size:12]; - NSAttributedString *highlighted = [RCTRedBox2AnsiParser attributedStringFromAnsiText:errorData.codeFrame - baseFont:codeFont - baseColor:[UIColor whiteColor]]; + NSAttributedString *highlighted = + [RCTRedBox2AnsiParser attributedStringFromAnsiText:errorData.codeFrame + baseFont:codeFont + baseColor:[RCTUIColor whiteColor]]; // [macOS] - UILabel *codeLabel = [[UILabel alloc] init]; - codeLabel.translatesAutoresizingMaskIntoConstraints = NO; + RCTUILabel *codeLabel = [self makeLabel]; // [macOS] +#if !TARGET_OS_OSX // [macOS] codeLabel.attributedText = highlighted; - codeLabel.numberOfLines = 0; +#else // [macOS + codeLabel.attributedStringValue = highlighted; +#endif // macOS] codeLabel.lineBreakMode = NSLineBreakByClipping; +#if !TARGET_OS_OSX // [macOS] UIScrollView *codeScrollView = [[UIScrollView alloc] init]; codeScrollView.translatesAutoresizingMaskIntoConstraints = NO; codeScrollView.showsHorizontalScrollIndicator = YES; @@ -920,10 +690,13 @@ - (UITableViewCell *)reuseCell:(UITableViewCell *)cell forCodeFrame:(RCTRedBox2E codeScrollView.bounces = NO; [codeScrollView addSubview:codeLabel]; [container addSubview:codeScrollView]; +#else // [macOS + [container addSubview:codeLabel]; +#endif // macOS] // File name label below the code frame - UILabel *fileLabel = [[UILabel alloc] init]; - fileLabel.translatesAutoresizingMaskIntoConstraints = NO; + RCTUILabel *fileLabel = [self makeLabel]; // [macOS] + fileLabel.numberOfLines = 1; NSString *fileName = errorData.codeFrameFileName.lastPathComponent ?: errorData.codeFrameFileName; if (errorData.codeFrameRow > 0) { fileLabel.text = [NSString @@ -941,6 +714,7 @@ - (UITableViewCell *)reuseCell:(UITableViewCell *)cell forCodeFrame:(RCTRedBox2E [container.leadingAnchor constraintEqualToAnchor:cell.contentView.leadingAnchor constant:10], [container.trailingAnchor constraintEqualToAnchor:cell.contentView.trailingAnchor constant:-10], +#if !TARGET_OS_OSX // [macOS] [codeScrollView.topAnchor constraintEqualToAnchor:container.topAnchor constant:10], [codeScrollView.leadingAnchor constraintEqualToAnchor:container.leadingAnchor constant:10], [codeScrollView.trailingAnchor constraintEqualToAnchor:container.trailingAnchor constant:-10], @@ -951,6 +725,12 @@ - (UITableViewCell *)reuseCell:(UITableViewCell *)cell forCodeFrame:(RCTRedBox2E [codeLabel.trailingAnchor constraintEqualToAnchor:codeScrollView.trailingAnchor], [codeLabel.bottomAnchor constraintEqualToAnchor:codeScrollView.bottomAnchor], [codeLabel.heightAnchor constraintEqualToAnchor:codeScrollView.heightAnchor], +#else // [macOS + [codeLabel.topAnchor constraintEqualToAnchor:container.topAnchor constant:10], + [codeLabel.leadingAnchor constraintEqualToAnchor:container.leadingAnchor constant:10], + [codeLabel.trailingAnchor constraintEqualToAnchor:container.trailingAnchor constant:-10], + [codeLabel.bottomAnchor constraintEqualToAnchor:container.bottomAnchor constant:-10], +#endif // macOS] [fileLabel.topAnchor constraintEqualToAnchor:container.bottomAnchor constant:10], [fileLabel.leadingAnchor constraintEqualToAnchor:cell.contentView.leadingAnchor constant:10], @@ -961,16 +741,17 @@ - (UITableViewCell *)reuseCell:(UITableViewCell *)cell forCodeFrame:(RCTRedBox2E return cell; } -- (CGFloat)tableView:(__unused UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath +- (CGFloat)tableView:(__unused RCTUITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath // [macOS] { auto section = [self sectionForIndex:indexPath.section]; if (section == Section::Message || section == Section::CodeFrame) { - return UITableViewAutomaticDimension; + return RCTUITableViewAutomaticDimension; // [macOS] } return 50; } -- (CGFloat)tableView:(__unused UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath +- (CGFloat)tableView:(__unused RCTUITableView *)tableView // [macOS] + estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath { switch ([self sectionForIndex:indexPath.section]) { case Section::Message: @@ -983,15 +764,14 @@ - (CGFloat)tableView:(__unused UITableView *)tableView estimatedHeightForRowAtIn } } -- (UIView *)sectionHeaderViewWithTitle:(NSString *)title +- (RCTPlatformView *)sectionHeaderViewWithTitle:(NSString *)title // [macOS] { - UIView *headerView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 0, 38)]; - headerView.backgroundColor = [UIColor clearColor]; + RCTUIView *headerView = [[RCTUIView alloc] initWithFrame:CGRectMake(0, 0, 0, 38)]; // [macOS] + headerView.backgroundColor = [RCTUIColor clearColor]; // [macOS] - UILabel *label = [[UILabel alloc] init]; - label.translatesAutoresizingMaskIntoConstraints = NO; + RCTUILabel *label = [self makeLabel]; // [macOS] label.text = title; - label.textColor = [UIColor whiteColor]; + label.textColor = [RCTUIColor whiteColor]; // [macOS] label.font = [UIFont systemFontOfSize:18 weight:UIFontWeightSemibold]; [headerView addSubview:label]; @@ -1004,7 +784,8 @@ - (UIView *)sectionHeaderViewWithTitle:(NSString *)title return headerView; } -- (UIView *)tableView:(__unused UITableView *)tableView viewForHeaderInSection:(NSInteger)section +- (nullable RCTPlatformView *)tableView:(__unused RCTUITableView *)tableView // [macOS] + viewForHeaderInSection:(NSInteger)section { switch ([self sectionForIndex:section]) { case Section::CodeFrame: @@ -1017,13 +798,13 @@ - (UIView *)tableView:(__unused UITableView *)tableView viewForHeaderInSection:( } } -- (CGFloat)tableView:(__unused UITableView *)tableView heightForHeaderInSection:(NSInteger)section +- (CGFloat)tableView:(__unused RCTUITableView *)tableView heightForHeaderInSection:(NSInteger)section // [macOS] { auto s = [self sectionForIndex:section]; return (s == Section::CodeFrame || s == Section::CallStack) ? 38 : 0; } -- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath +- (void)tableView:(RCTUITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath // [macOS] { if ([self sectionForIndex:indexPath.section] == Section::CallStack) { NSUInteger row = indexPath.row; @@ -1032,259 +813,6 @@ - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath } [tableView deselectRowAtIndexPath:indexPath animated:YES]; } -#else // [macOS - -// macOS AppKit NSTableView has no notion of sections, so the RedBox 2.0 sections -// (Message, optional Source/Code Frame, optional Call Stack) are flattened into a -// single list of rows, with lightweight header rows for the Source/Call Stack groups. -typedef NS_ENUM(NSInteger, RCTRedBox2MacRowKind) { - RCTRedBox2MacRowKindMessage, - RCTRedBox2MacRowKindSourceHeader, - RCTRedBox2MacRowKindCodeFrame, - RCTRedBox2MacRowKindCallStackHeader, - RCTRedBox2MacRowKindStackFrame, -}; - -- (RCTRedBox2MacRowKind)macRowKindForRow:(NSInteger)row stackIndex:(NSInteger *)outStackIndex -{ - if (outStackIndex != nullptr) { - *outStackIndex = 0; - } - NSInteger current = 0; - if (_sectionStates[static_cast(Section::Message)].visible) { - if (row == current) { - return RCTRedBox2MacRowKindMessage; - } - current++; - } - if (_sectionStates[static_cast(Section::CodeFrame)].visible) { - if (row == current) { - return RCTRedBox2MacRowKindSourceHeader; - } - current++; - if (row == current) { - return RCTRedBox2MacRowKindCodeFrame; - } - current++; - } - if (_sectionStates[static_cast(Section::CallStack)].visible) { - if (row == current) { - return RCTRedBox2MacRowKindCallStackHeader; - } - current++; - if (outStackIndex != nullptr) { - *outStackIndex = row - current; - } - return RCTRedBox2MacRowKindStackFrame; - } - return RCTRedBox2MacRowKindMessage; -} - -- (NSInteger)numberOfRowsInTableView:(__unused NSTableView *)tableView -{ - NSInteger count = 0; - if (_sectionStates[static_cast(Section::Message)].visible) { - count += 1; - } - if (_sectionStates[static_cast(Section::CodeFrame)].visible) { - count += 2; // "Source" header + code frame - } - if (_sectionStates[static_cast(Section::CallStack)].visible) { - count += 1 + static_cast(_lastStackTrace.count); // "Call Stack" header + frames - } - return count; -} - -- (nullable NSView *)tableView:(__unused NSTableView *)tableView - viewForTableColumn:(nullable __unused NSTableColumn *)tableColumn - row:(NSInteger)row -{ - NSInteger stackIndex = 0; - switch ([self macRowKindForRow:row stackIndex:&stackIndex]) { - case RCTRedBox2MacRowKindMessage: - return [self macMessageCell]; - case RCTRedBox2MacRowKindSourceHeader: - return [self macHeaderCellWithTitle:@"Source"]; - case RCTRedBox2MacRowKindCodeFrame: - return [self macCodeFrameCell:_errorData]; - case RCTRedBox2MacRowKindCallStackHeader: - return [self macHeaderCellWithTitle:@"Call Stack"]; - case RCTRedBox2MacRowKindStackFrame: - return [self macStackFrameCell:_lastStackTrace[stackIndex]]; - } - return nil; -} - -- (NSView *)macMessageCell -{ - NSView *cell = [[NSView alloc] initWithFrame:NSZeroRect]; - cell.wantsLayer = YES; - cell.layer.backgroundColor = RCTRedBox2BackgroundColor().CGColor; - - NSTextField *categoryLabel = [self makeLabel]; - categoryLabel.textColor = RCTRedBox2ErrorColor(); - categoryLabel.font = [NSFont systemFontOfSize:21 weight:NSFontWeightBold]; - categoryLabel.maximumNumberOfLines = 1; - categoryLabel.stringValue = _errorData.title ?: @""; - [cell addSubview:categoryLabel]; - - NSTextField *messageLabel = [self makeLabel]; - messageLabel.accessibilityIdentifier = @"redbox-error"; - messageLabel.textColor = [NSColor whiteColor]; - messageLabel.font = [NSFont systemFontOfSize:14 weight:NSFontWeightMedium]; - messageLabel.stringValue = [self displayMessage] ?: @""; - [cell addSubview:messageLabel]; - - [NSLayoutConstraint activateConstraints:@[ - [categoryLabel.topAnchor constraintEqualToAnchor:cell.topAnchor constant:15], - [categoryLabel.leadingAnchor constraintEqualToAnchor:cell.leadingAnchor constant:12], - [categoryLabel.trailingAnchor constraintEqualToAnchor:cell.trailingAnchor constant:-12], - - [messageLabel.topAnchor constraintEqualToAnchor:categoryLabel.bottomAnchor constant:10], - [messageLabel.leadingAnchor constraintEqualToAnchor:cell.leadingAnchor constant:12], - [messageLabel.trailingAnchor constraintEqualToAnchor:cell.trailingAnchor constant:-12], - [messageLabel.bottomAnchor constraintEqualToAnchor:cell.bottomAnchor constant:-15], - ]]; - - return cell; -} - -- (NSView *)macHeaderCellWithTitle:(NSString *)title -{ - NSView *cell = [[NSView alloc] initWithFrame:NSZeroRect]; - - NSTextField *label = [self makeLabel]; - label.textColor = [NSColor whiteColor]; - label.font = [NSFont systemFontOfSize:18 weight:NSFontWeightSemibold]; - label.maximumNumberOfLines = 1; - label.stringValue = title; - [cell addSubview:label]; - - [NSLayoutConstraint activateConstraints:@[ - [label.leadingAnchor constraintEqualToAnchor:cell.leadingAnchor constant:12], - [label.trailingAnchor constraintEqualToAnchor:cell.trailingAnchor constant:-12], - [label.topAnchor constraintEqualToAnchor:cell.topAnchor constant:8], - [label.bottomAnchor constraintEqualToAnchor:cell.bottomAnchor constant:-10], - ]]; - - return cell; -} - -- (NSView *)macCodeFrameCell:(RCTRedBox2ErrorData *)errorData -{ - NSView *cell = [[NSView alloc] initWithFrame:NSZeroRect]; - - NSView *container = [[NSView alloc] initWithFrame:NSZeroRect]; - container.translatesAutoresizingMaskIntoConstraints = NO; - container.wantsLayer = YES; - container.layer.backgroundColor = RCTRedBox2BackgroundColor().CGColor; - container.layer.cornerRadius = 3; - [cell addSubview:container]; - - // Render code frame with ANSI syntax highlighting - NSFont *codeFont = [NSFont fontWithName:@"Menlo-Regular" size:12]; - NSAttributedString *highlighted = [RCTRedBox2AnsiParser attributedStringFromAnsiText:errorData.codeFrame - baseFont:codeFont - baseColor:[NSColor whiteColor]]; - - NSTextField *codeLabel = [self makeLabel]; - codeLabel.attributedStringValue = highlighted; - codeLabel.lineBreakMode = NSLineBreakByClipping; - [container addSubview:codeLabel]; - - // File name label below the code frame - NSTextField *fileLabel = [self makeLabel]; - NSString *fileName = errorData.codeFrameFileName.lastPathComponent ?: errorData.codeFrameFileName; - if (errorData.codeFrameRow > 0) { - fileLabel.stringValue = [NSString - stringWithFormat:@"%@ (%ld:%ld)", fileName, (long)errorData.codeFrameRow, (long)errorData.codeFrameColumn + 1]; - } else if (fileName.length > 0) { - fileLabel.stringValue = fileName; - } - fileLabel.textColor = RCTRedBox2TextColor(0.5); - fileLabel.font = [NSFont fontWithName:@"Menlo-Regular" size:12]; - fileLabel.alignment = NSTextAlignmentCenter; - fileLabel.maximumNumberOfLines = 1; - [cell addSubview:fileLabel]; - - [NSLayoutConstraint activateConstraints:@[ - [container.topAnchor constraintEqualToAnchor:cell.topAnchor constant:5], - [container.leadingAnchor constraintEqualToAnchor:cell.leadingAnchor constant:10], - [container.trailingAnchor constraintEqualToAnchor:cell.trailingAnchor constant:-10], - - [codeLabel.topAnchor constraintEqualToAnchor:container.topAnchor constant:10], - [codeLabel.leadingAnchor constraintEqualToAnchor:container.leadingAnchor constant:10], - [codeLabel.trailingAnchor constraintEqualToAnchor:container.trailingAnchor constant:-10], - [codeLabel.bottomAnchor constraintEqualToAnchor:container.bottomAnchor constant:-10], - - [fileLabel.topAnchor constraintEqualToAnchor:container.bottomAnchor constant:10], - [fileLabel.leadingAnchor constraintEqualToAnchor:cell.leadingAnchor constant:10], - [fileLabel.trailingAnchor constraintEqualToAnchor:cell.trailingAnchor constant:-10], - [fileLabel.bottomAnchor constraintEqualToAnchor:cell.bottomAnchor constant:-10], - ]]; - - return cell; -} - -- (NSView *)macStackFrameCell:(RCTJSStackFrame *)stackFrame -{ - NSView *cell = [[NSView alloc] initWithFrame:NSZeroRect]; - - NSTextField *label = [self makeLabel]; - label.maximumNumberOfLines = 2; - [cell addSubview:label]; - - NSMutableParagraphStyle *textParagraphStyle = [NSMutableParagraphStyle new]; - textParagraphStyle.lineBreakMode = NSLineBreakByCharWrapping; - - NSDictionary *textAttributes = @{ - NSForegroundColorAttributeName : stackFrame.collapse ? RCTRedBox2TextColor(0.4) : [NSColor whiteColor], - NSFontAttributeName : [NSFont fontWithName:@"Menlo-Regular" size:14], - NSParagraphStyleAttributeName : textParagraphStyle, - }; - NSString *text = stackFrame.methodName ?: @"(unnamed method)"; - NSMutableAttributedString *title = [[NSMutableAttributedString alloc] initWithString:text attributes:textAttributes]; - - if (stackFrame.file != nullptr) { - label.maximumNumberOfLines = 3; - - NSMutableParagraphStyle *detailParagraphStyle = [NSMutableParagraphStyle new]; - detailParagraphStyle.lineBreakMode = NSLineBreakByTruncatingMiddle; - - NSDictionary *detailAttributes = @{ - NSForegroundColorAttributeName : stackFrame.collapse ? RCTRedBox2TextColor(0.3) : RCTRedBox2TextColor(0.8), - NSFontAttributeName : [NSFont systemFontOfSize:12 weight:NSFontWeightLight], - NSParagraphStyleAttributeName : detailParagraphStyle, - }; - NSAttributedString *detail = [[NSAttributedString alloc] initWithString:[self formatFrameSource:stackFrame] - attributes:detailAttributes]; - [title appendAttributedString:[[NSAttributedString alloc] initWithString:@"\n"]]; - [title appendAttributedString:detail]; - } - - label.attributedStringValue = title; - - [NSLayoutConstraint activateConstraints:@[ - [label.leadingAnchor constraintEqualToAnchor:cell.leadingAnchor constant:12], - [label.trailingAnchor constraintEqualToAnchor:cell.trailingAnchor constant:-12], - [label.topAnchor constraintEqualToAnchor:cell.topAnchor constant:8], - [label.bottomAnchor constraintEqualToAnchor:cell.bottomAnchor constant:-8], - ]]; - - return cell; -} - -- (BOOL)tableView:(__unused NSTableView *)tableView shouldSelectRow:(NSInteger)row -{ - NSInteger stackIndex = 0; - if ([self macRowKindForRow:row stackIndex:&stackIndex] == RCTRedBox2MacRowKindStackFrame) { - RCTJSStackFrame *stackFrame = _lastStackTrace[stackIndex]; - [_actionDelegate redBoxController:self openStackFrameInEditor:stackFrame]; - } - return NO; -} -#endif // macOS] - #pragma mark - Key Commands #if !TARGET_OS_OSX // [macOS]