From 8f279b5d2ad0b73862bac34564ccf6a74a134fc4 Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Tue, 28 Jul 2026 12:42:03 -0700 Subject: [PATCH 01/14] feat(macos): add RCTUITableView compatibility primitive to RCTUIKit Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ReactApple/Libraries/RCTUIKit/RCTUIKit.h | 1 + .../Libraries/RCTUIKit/RCTUITableView.h | 112 +++++ .../Libraries/RCTUIKit/RCTUITableView.m | 431 ++++++++++++++++++ 3 files changed, 544 insertions(+) create mode 100644 packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUITableView.h create mode 100644 packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUITableView.m diff --git a/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUIKit.h b/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUIKit.h index eccebc648400..e1a2f0ae0228 100644 --- a/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUIKit.h +++ b/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUIKit.h @@ -16,6 +16,7 @@ #import "RCTUIView.h" #import "RCTUIScrollView.h" #import "RCTUISlider.h" +#import "RCTUITableView.h" #import "RCTUILabel.h" #import "RCTUISwitch.h" #import "RCTUIActivityIndicatorView.h" diff --git a/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUITableView.h b/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUITableView.h new file mode 100644 index 000000000000..d451aca2d542 --- /dev/null +++ b/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUITableView.h @@ -0,0 +1,112 @@ +/* + * 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 "RCTUILabel.h" +#import "RCTUIView.h" + +#if !TARGET_OS_OSX + +#import + +@compatibility_alias RCTUITableView UITableView; +@compatibility_alias RCTUITableViewCell UITableViewCell; +#define RCTUITableViewDataSource UITableViewDataSource +#define RCTUITableViewDelegate UITableViewDelegate +#define RCTUITableViewCellStyleDefault UITableViewCellStyleDefault +#define RCTUITableViewCellStyleSubtitle UITableViewCellStyleSubtitle +#define RCTUITableViewScrollPositionTop UITableViewScrollPositionTop +#define RCTUITableViewAutomaticDimension UITableViewAutomaticDimension + +#else // TARGET_OS_OSX [ + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class RCTUITableView; + +typedef NS_ENUM(NSInteger, RCTUITableViewCellStyle) { + RCTUITableViewCellStyleDefault, + RCTUITableViewCellStyleSubtitle, +}; + +typedef NS_ENUM(NSInteger, RCTUITableViewScrollPosition) { + RCTUITableViewScrollPositionTop, +}; + +extern const CGFloat RCTUITableViewAutomaticDimension; + +@interface NSIndexPath (RCTUITableView) + +@property (nonatomic, readonly) NSInteger row; ++ (instancetype)indexPathForRow:(NSInteger)row inSection:(NSInteger)section; + +@end + +@interface RCTUITableViewCell : NSTableCellView + +- (instancetype)initWithStyle:(RCTUITableViewCellStyle)style + reuseIdentifier:(nullable NSString *)reuseIdentifier NS_DESIGNATED_INITIALIZER; +- (nullable instancetype)initWithCoder:(NSCoder *)coder NS_UNAVAILABLE; + +@property (nonatomic, readonly, nullable, copy) NSString *reuseIdentifier; +@property (nonatomic, readonly, strong) RCTPlatformView *contentView; +@property (nonatomic, readonly, strong) RCTUILabel *textLabel; +@property (nonatomic, readonly, nullable, strong) RCTUILabel *detailTextLabel; +@property (nonatomic, nullable, strong) RCTUIColor *backgroundColor; + +- (void)prepareForReuse; + +@end + +@protocol RCTUITableViewDataSource + +@required +- (NSInteger)tableView:(RCTUITableView *)tableView numberOfRowsInSection:(NSInteger)section; +- (RCTUITableViewCell *)tableView:(RCTUITableView *)tableView + cellForRowAtIndexPath:(NSIndexPath *)indexPath; + +@optional +- (NSInteger)numberOfSectionsInTableView:(RCTUITableView *)tableView; + +@end + +@protocol RCTUITableViewDelegate + +@optional +- (CGFloat)tableView:(RCTUITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath; +- (nullable RCTPlatformView *)tableView:(RCTUITableView *)tableView viewForHeaderInSection:(NSInteger)section; +- (CGFloat)tableView:(RCTUITableView *)tableView heightForHeaderInSection:(NSInteger)section; +- (void)tableView:(RCTUITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath; + +@end + +@interface RCTUITableView : NSScrollView + +@property (nonatomic, weak, nullable) id dataSource; +@property (nonatomic, weak, nullable) id delegate; +@property (nonatomic, nullable, copy) RCTUIColor *separatorColor; +@property (nonatomic, readonly) CGRect visibleRect; + +- (void)reloadData; +- (nullable __kindof RCTUITableViewCell *)dequeueReusableCellWithIdentifier:(NSString *)identifier; +- (void)scrollToRowAtIndexPath:(NSIndexPath *)indexPath + atScrollPosition:(RCTUITableViewScrollPosition)position + animated:(BOOL)animated; +- (void)deselectRowAtIndexPath:(NSIndexPath *)indexPath animated:(BOOL)animated; + +@end + +NS_ASSUME_NONNULL_END + +#endif // ] TARGET_OS_OSX diff --git a/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUITableView.m b/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUITableView.m new file mode 100644 index 000000000000..ec2e5741e8c6 --- /dev/null +++ b/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUITableView.m @@ -0,0 +1,431 @@ +/* + * 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 "RCTUITableView.h" + +#if TARGET_OS_OSX + +const CGFloat RCTUITableViewAutomaticDimension = -1.0; + +typedef NS_ENUM(NSInteger, RCTUITableViewSlotKind) { + RCTUITableViewSlotKindHeader, + RCTUITableViewSlotKindRow, +}; + +@interface RCTUITableViewSlot : NSObject + +@property (nonatomic, readonly) RCTUITableViewSlotKind kind; +@property (nonatomic, readonly) NSInteger section; +@property (nonatomic, readonly) NSInteger row; +@property (nonatomic, readonly) CGFloat headerHeight; + +- (instancetype)initWithKind:(RCTUITableViewSlotKind)kind + section:(NSInteger)section + row:(NSInteger)row + headerHeight:(CGFloat)headerHeight; + +@end + +@implementation RCTUITableViewSlot + +- (instancetype)initWithKind:(RCTUITableViewSlotKind)kind + section:(NSInteger)section + row:(NSInteger)row + headerHeight:(CGFloat)headerHeight +{ + if (self = [super init]) { + _kind = kind; + _section = section; + _row = row; + _headerHeight = headerHeight; + } + + return self; +} + +@end + +@implementation NSIndexPath (RCTUITableView) + +- (NSInteger)row +{ + return self.item; +} + ++ (instancetype)indexPathForRow:(NSInteger)row inSection:(NSInteger)section +{ + return [self indexPathForItem:row inSection:section]; +} + +@end + +@interface RCTUITableViewCell () + +@property (nonatomic, readwrite, nullable, copy) NSString *reuseIdentifier; +@property (nonatomic, readwrite, strong) RCTPlatformView *contentView; +@property (nonatomic, readwrite, strong) RCTUILabel *textLabel; +@property (nonatomic, readwrite, nullable, strong) RCTUILabel *detailTextLabel; + +@end + +@implementation RCTUITableViewCell { + RCTUITableViewCellStyle _style; +} + +- (instancetype)initWithFrame:(NSRect)frameRect +{ + self = [self initWithStyle:RCTUITableViewCellStyleDefault reuseIdentifier:nil]; + self.frame = frameRect; + return self; +} + +- (instancetype)initWithStyle:(RCTUITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier +{ + if (self = [super initWithFrame:NSZeroRect]) { + [self initializeWithStyle:style reuseIdentifier:reuseIdentifier]; + } + + return self; +} + +- (void)initializeWithStyle:(RCTUITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier +{ + _style = style; + _reuseIdentifier = [reuseIdentifier copy]; + self.identifier = reuseIdentifier; + + _contentView = [[RCTPlatformView alloc] initWithFrame:self.bounds]; + _contentView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable; + [self addSubview:_contentView]; + + _textLabel = [[RCTUILabel alloc] initWithFrame:NSZeroRect]; + _textLabel.translatesAutoresizingMaskIntoConstraints = NO; + [_contentView addSubview:_textLabel]; + self.textField = _textLabel; + + if (style == RCTUITableViewCellStyleSubtitle) { + _detailTextLabel = [[RCTUILabel alloc] initWithFrame:NSZeroRect]; + _detailTextLabel.translatesAutoresizingMaskIntoConstraints = NO; + [_contentView addSubview:_detailTextLabel]; + + [NSLayoutConstraint activateConstraints:@[ + [_textLabel.leadingAnchor constraintEqualToAnchor:_contentView.leadingAnchor constant:5], + [_textLabel.topAnchor constraintEqualToAnchor:_contentView.topAnchor], + [_textLabel.trailingAnchor constraintEqualToAnchor:_contentView.trailingAnchor constant:-5], + [_detailTextLabel.leadingAnchor constraintEqualToAnchor:_contentView.leadingAnchor constant:5], + [_detailTextLabel.topAnchor constraintEqualToAnchor:_textLabel.bottomAnchor], + [_detailTextLabel.trailingAnchor constraintEqualToAnchor:_contentView.trailingAnchor constant:-5], + [_detailTextLabel.bottomAnchor constraintEqualToAnchor:_contentView.bottomAnchor], + ]]; + } else { + [NSLayoutConstraint activateConstraints:@[ + [_textLabel.leadingAnchor constraintEqualToAnchor:_contentView.leadingAnchor constant:5], + [_textLabel.topAnchor constraintEqualToAnchor:_contentView.topAnchor constant:5], + [_textLabel.trailingAnchor constraintEqualToAnchor:_contentView.trailingAnchor constant:-5], + [_textLabel.bottomAnchor constraintEqualToAnchor:_contentView.bottomAnchor constant:-5], + ]]; + } +} + +- (void)setBackgroundColor:(RCTUIColor *)backgroundColor +{ + _backgroundColor = [backgroundColor copy]; + self.contentView.wantsLayer = YES; + self.contentView.layer.backgroundColor = backgroundColor.CGColor; +} + +- (void)setAccessibilityIdentifier:(NSString *)accessibilityIdentifier +{ + [super setAccessibilityIdentifier:accessibilityIdentifier]; + self.textLabel.accessibilityIdentifier = accessibilityIdentifier; +} + +- (void)setAccessibilityLabel:(NSString *)accessibilityLabel +{ + [super setAccessibilityLabel:accessibilityLabel]; + self.textLabel.accessibilityLabel = accessibilityLabel; +} + +- (void)prepareForReuse +{ + [super prepareForReuse]; + self.textLabel.text = @""; + self.textLabel.maximumNumberOfLines = _style == RCTUITableViewCellStyleDefault ? 1 : 2; + self.detailTextLabel.text = @""; + self.detailTextLabel.maximumNumberOfLines = 1; +} + +@end + +@interface RCTUITableView () +@end + +@implementation RCTUITableView { + NSTableView *_tableView; + NSArray *_slots; + NSMutableDictionary *_headerViews; + NSMutableDictionary *_pendingViews; + NSMutableDictionary *_automaticHeights; + NSMutableIndexSet *_automaticRows; + CGFloat _lastContentWidth; +} + +- (instancetype)initWithFrame:(NSRect)frameRect +{ + if (self = [super initWithFrame:frameRect]) { + self.drawsBackground = NO; + + _slots = @[]; + _headerViews = [NSMutableDictionary new]; + _pendingViews = [NSMutableDictionary new]; + _automaticHeights = [NSMutableDictionary new]; + _automaticRows = [NSMutableIndexSet new]; + + _tableView = [[NSTableView alloc] initWithFrame:self.contentView.bounds]; + _tableView.autoresizingMask = NSViewWidthSizable; + _tableView.dataSource = self; + _tableView.delegate = self; + _tableView.headerView = nil; + _tableView.allowsColumnReordering = NO; + _tableView.allowsColumnResizing = NO; + _tableView.allowsTypeSelect = NO; + _tableView.columnAutoresizingStyle = NSTableViewFirstColumnOnlyAutoresizingStyle; + _tableView.backgroundColor = NSColor.clearColor; + _tableView.style = NSTableViewStyleInset; + _tableView.accessibilityRole = NSAccessibilityTableRole; + + NSTableColumn *column = [[NSTableColumn alloc] initWithIdentifier:@"RCTUITableViewColumn"]; + [_tableView addTableColumn:column]; + + self.documentView = _tableView; + _lastContentWidth = self.contentSize.width; + self.separatorColor = nil; + } + + return self; +} + +- (void)setAccessibilityIdentifier:(NSString *)accessibilityIdentifier +{ + [super setAccessibilityIdentifier:accessibilityIdentifier]; + _tableView.accessibilityIdentifier = accessibilityIdentifier; +} + +- (void)setSeparatorColor:(RCTUIColor *)separatorColor +{ + _separatorColor = [separatorColor copy]; + if (separatorColor == nil) { + _tableView.gridStyleMask = NSTableViewGridNone; + } else { + _tableView.gridColor = separatorColor; + _tableView.gridStyleMask = NSTableViewSolidHorizontalGridLineMask; + } +} + +- (CGRect)visibleRect +{ + return _tableView.visibleRect; +} + +- (void)setFrameSize:(NSSize)newSize +{ + [super setFrameSize:newSize]; + + CGFloat contentWidth = self.contentSize.width; + if (_lastContentWidth != contentWidth) { + _lastContentWidth = contentWidth; + [_automaticHeights removeAllObjects]; + if (_automaticRows.count > 0) { + [_tableView noteHeightOfRowsWithIndexesChanged:_automaticRows]; + } + } +} + +- (void)reloadData +{ + [_headerViews removeAllObjects]; + [_pendingViews removeAllObjects]; + [_automaticHeights removeAllObjects]; + [_automaticRows removeAllIndexes]; + + NSInteger sectionCount = 1; + if ([self.dataSource respondsToSelector:@selector(numberOfSectionsInTableView:)]) { + sectionCount = [self.dataSource numberOfSectionsInTableView:self]; + } + sectionCount = MAX(sectionCount, 0); + + NSMutableArray *slots = [NSMutableArray new]; + for (NSInteger section = 0; section < sectionCount; section++) { + if ([self.delegate respondsToSelector:@selector(tableView:heightForHeaderInSection:)]) { + CGFloat headerHeight = [self.delegate tableView:self heightForHeaderInSection:section]; + if (headerHeight > 0) { + [slots addObject:[[RCTUITableViewSlot alloc] initWithKind:RCTUITableViewSlotKindHeader + section:section + row:NSNotFound + headerHeight:headerHeight]]; + } + } + + NSInteger rowCount = MAX([self.dataSource tableView:self numberOfRowsInSection:section], 0); + for (NSInteger row = 0; row < rowCount; row++) { + [slots addObject:[[RCTUITableViewSlot alloc] initWithKind:RCTUITableViewSlotKindRow + section:section + row:row + headerHeight:0]]; + } + } + + _slots = [slots copy]; + [_tableView reloadData]; +} + +- (__kindof RCTUITableViewCell *)dequeueReusableCellWithIdentifier:(NSString *)identifier +{ + RCTUITableViewCell *cell = [_tableView makeViewWithIdentifier:identifier owner:self]; + [cell prepareForReuse]; + return cell; +} + +- (void)scrollToRowAtIndexPath:(NSIndexPath *)indexPath + atScrollPosition:(RCTUITableViewScrollPosition)position + animated:(BOOL)animated +{ + NSInteger backingRow = [self backingRowForIndexPath:indexPath]; + if (backingRow == NSNotFound || position != RCTUITableViewScrollPositionTop) { + return; + } + + NSRect rowRect = [_tableView rectOfRow:backingRow]; + NSClipView *clipView = self.contentView; + NSRect proposedBounds = clipView.bounds; + proposedBounds.origin.y = NSMinY(rowRect); + NSRect constrainedBounds = [clipView constrainBoundsRect:proposedBounds]; + [clipView scrollToPoint:constrainedBounds.origin]; + [self reflectScrolledClipView:clipView]; +} + +- (void)deselectRowAtIndexPath:(NSIndexPath *)indexPath animated:(BOOL)animated +{ + NSInteger backingRow = [self backingRowForIndexPath:indexPath]; + if (backingRow != NSNotFound) { + [_tableView deselectRow:backingRow]; + } +} + +- (NSInteger)backingRowForIndexPath:(NSIndexPath *)indexPath +{ + for (NSInteger backingRow = 0; backingRow < (NSInteger)_slots.count; backingRow++) { + RCTUITableViewSlot *slot = _slots[backingRow]; + if (slot.kind == RCTUITableViewSlotKindRow && slot.section == indexPath.section && slot.row == indexPath.row) { + return backingRow; + } + } + return NSNotFound; +} + +- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView +{ + return _slots.count; +} + +- (NSView *)tableView:(NSTableView *)tableView + viewForTableColumn:(NSTableColumn *)tableColumn + row:(NSInteger)row +{ + NSNumber *rowKey = @(row); + NSView *pendingView = _pendingViews[rowKey]; + if (pendingView != nil) { + [_pendingViews removeObjectForKey:rowKey]; + return pendingView; + } + + return [self viewForBackingRow:row]; +} + +- (NSView *)viewForBackingRow:(NSInteger)backingRow +{ + RCTUITableViewSlot *slot = _slots[backingRow]; + if (slot.kind == RCTUITableViewSlotKindRow) { + NSIndexPath *indexPath = [NSIndexPath indexPathForRow:slot.row inSection:slot.section]; + return [self.dataSource tableView:self cellForRowAtIndexPath:indexPath]; + } + + NSNumber *sectionKey = @(slot.section); + RCTPlatformView *headerView = _headerViews[sectionKey]; + if (headerView == nil) { + if ([self.delegate respondsToSelector:@selector(tableView:viewForHeaderInSection:)]) { + headerView = [self.delegate tableView:self viewForHeaderInSection:slot.section]; + } + if (headerView == nil) { + headerView = [RCTPlatformView new]; + } + _headerViews[sectionKey] = headerView; + } + return headerView; +} + +- (CGFloat)tableView:(NSTableView *)tableView heightOfRow:(NSInteger)row +{ + RCTUITableViewSlot *slot = _slots[row]; + if (slot.kind == RCTUITableViewSlotKindHeader) { + return slot.headerHeight; + } + + CGFloat height = tableView.rowHeight; + if ([self.delegate respondsToSelector:@selector(tableView:heightForRowAtIndexPath:)]) { + NSIndexPath *indexPath = [NSIndexPath indexPathForRow:slot.row inSection:slot.section]; + height = [self.delegate tableView:self heightForRowAtIndexPath:indexPath]; + } + if (height != RCTUITableViewAutomaticDimension) { + return height; + } + + [_automaticRows addIndex:row]; + NSNumber *rowKey = @(row); + NSNumber *cachedHeight = _automaticHeights[rowKey]; + if (cachedHeight != nil) { + return cachedHeight.doubleValue; + } + + RCTUITableViewCell *cell = (RCTUITableViewCell *)[tableView viewAtColumn:0 row:row makeIfNecessary:NO]; + if (cell == nil) { + cell = (RCTUITableViewCell *)_pendingViews[rowKey]; + } + if (cell == nil) { + cell = (RCTUITableViewCell *)[self viewForBackingRow:row]; + _pendingViews[rowKey] = cell; + } + + NSSize previousSize = cell.frame.size; + cell.frame = NSMakeRect(0, 0, self.contentSize.width, previousSize.height); + [cell layoutSubtreeIfNeeded]; + CGFloat fittingHeight = cell.fittingSize.height; + cell.frame = NSMakeRect(cell.frame.origin.x, cell.frame.origin.y, previousSize.width, previousSize.height); + + _automaticHeights[rowKey] = @(fittingHeight); + return fittingHeight; +} + +- (BOOL)tableView:(NSTableView *)tableView shouldSelectRow:(NSInteger)row +{ + RCTUITableViewSlot *slot = _slots[row]; + if (slot.kind == RCTUITableViewSlotKindHeader) { + return NO; + } + + if ([self.delegate respondsToSelector:@selector(tableView:didSelectRowAtIndexPath:)]) { + NSIndexPath *indexPath = [NSIndexPath indexPathForRow:slot.row inSection:slot.section]; + [self.delegate tableView:self didSelectRowAtIndexPath:indexPath]; + } + return NO; +} + +@end + +#endif From 0758faedca3007c4125cf37c218483784911b95f Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Tue, 28 Jul 2026 12:42:28 -0700 Subject: [PATCH 02/14] fix(macos): implement RCTUILabel compatibility properties Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Libraries/RCTUIKit/RCTUILabel.m | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUILabel.m b/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUILabel.m index 919eb5641868..76d873d22349 100644 --- a/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUILabel.m +++ b/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUILabel.m @@ -31,6 +31,31 @@ - (void)setText:(NSString *)text [self setStringValue:text]; } +- (NSString *)text +{ + return self.stringValue; +} + +- (void)setNumberOfLines:(NSInteger)numberOfLines +{ + self.maximumNumberOfLines = numberOfLines; +} + +- (NSInteger)numberOfLines +{ + return self.maximumNumberOfLines; +} + +- (void)setTextAlignment:(NSTextAlignment)textAlignment +{ + self.alignment = textAlignment; +} + +- (NSTextAlignment)textAlignment +{ + return self.alignment; +} + @end #endif From c1e57841da140c371bdbca61c5a005ba20f1bc35 Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Tue, 28 Jul 2026 12:44:43 -0700 Subject: [PATCH 03/14] 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 000000000000..4f97eacf1078 --- /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 000000000000..d01c39f7fb46 --- /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 e1a2f0ae0228..7d51cf2dfe76 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 c2ebe70445ca732693e1c62110b4124f84873370 Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Tue, 28 Jul 2026 13:18:27 -0700 Subject: [PATCH 04/14] fix(macos): correct RCTUITableView lifecycle and sizing Use AppKit-owned reuse preparation and automatic row heights, remove per-row measurement retention, avoid visibleRect coordinate overriding, and guard deselection against selection-query re-entry. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Libraries/RCTUIKit/RCTUITableView.h | 1 - .../Libraries/RCTUIKit/RCTUITableView.m | 59 ++++--------------- 2 files changed, 13 insertions(+), 47 deletions(-) diff --git a/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUITableView.h b/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUITableView.h index d451aca2d542..1abaf797f52b 100644 --- a/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUITableView.h +++ b/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUITableView.h @@ -96,7 +96,6 @@ extern const CGFloat RCTUITableViewAutomaticDimension; @property (nonatomic, weak, nullable) id dataSource; @property (nonatomic, weak, nullable) id delegate; @property (nonatomic, nullable, copy) RCTUIColor *separatorColor; -@property (nonatomic, readonly) CGRect visibleRect; - (void)reloadData; - (nullable __kindof RCTUITableViewCell *)dequeueReusableCellWithIdentifier:(NSString *)identifier; diff --git a/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUITableView.m b/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUITableView.m index ec2e5741e8c6..2948c30db97d 100644 --- a/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUITableView.m +++ b/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUITableView.m @@ -161,6 +161,15 @@ - (void)prepareForReuse self.detailTextLabel.maximumNumberOfLines = 1; } +- (void)layout +{ + [super layout]; + + CGFloat preferredWidth = MAX(NSWidth(self.contentView.bounds) - 10, 0); + self.textLabel.preferredMaxLayoutWidth = preferredWidth; + self.detailTextLabel.preferredMaxLayoutWidth = preferredWidth; +} + @end @interface RCTUITableView () @@ -170,8 +179,6 @@ @implementation RCTUITableView { NSTableView *_tableView; NSArray *_slots; NSMutableDictionary *_headerViews; - NSMutableDictionary *_pendingViews; - NSMutableDictionary *_automaticHeights; NSMutableIndexSet *_automaticRows; CGFloat _lastContentWidth; } @@ -183,8 +190,6 @@ - (instancetype)initWithFrame:(NSRect)frameRect _slots = @[]; _headerViews = [NSMutableDictionary new]; - _pendingViews = [NSMutableDictionary new]; - _automaticHeights = [NSMutableDictionary new]; _automaticRows = [NSMutableIndexSet new]; _tableView = [[NSTableView alloc] initWithFrame:self.contentView.bounds]; @@ -198,6 +203,7 @@ - (instancetype)initWithFrame:(NSRect)frameRect _tableView.columnAutoresizingStyle = NSTableViewFirstColumnOnlyAutoresizingStyle; _tableView.backgroundColor = NSColor.clearColor; _tableView.style = NSTableViewStyleInset; + _tableView.usesAutomaticRowHeights = YES; _tableView.accessibilityRole = NSAccessibilityTableRole; NSTableColumn *column = [[NSTableColumn alloc] initWithIdentifier:@"RCTUITableViewColumn"]; @@ -228,11 +234,6 @@ - (void)setSeparatorColor:(RCTUIColor *)separatorColor } } -- (CGRect)visibleRect -{ - return _tableView.visibleRect; -} - - (void)setFrameSize:(NSSize)newSize { [super setFrameSize:newSize]; @@ -240,7 +241,6 @@ - (void)setFrameSize:(NSSize)newSize CGFloat contentWidth = self.contentSize.width; if (_lastContentWidth != contentWidth) { _lastContentWidth = contentWidth; - [_automaticHeights removeAllObjects]; if (_automaticRows.count > 0) { [_tableView noteHeightOfRowsWithIndexesChanged:_automaticRows]; } @@ -250,8 +250,6 @@ - (void)setFrameSize:(NSSize)newSize - (void)reloadData { [_headerViews removeAllObjects]; - [_pendingViews removeAllObjects]; - [_automaticHeights removeAllObjects]; [_automaticRows removeAllIndexes]; NSInteger sectionCount = 1; @@ -287,9 +285,7 @@ - (void)reloadData - (__kindof RCTUITableViewCell *)dequeueReusableCellWithIdentifier:(NSString *)identifier { - RCTUITableViewCell *cell = [_tableView makeViewWithIdentifier:identifier owner:self]; - [cell prepareForReuse]; - return cell; + return [_tableView makeViewWithIdentifier:identifier owner:self]; } - (void)scrollToRowAtIndexPath:(NSIndexPath *)indexPath @@ -313,7 +309,7 @@ - (void)scrollToRowAtIndexPath:(NSIndexPath *)indexPath - (void)deselectRowAtIndexPath:(NSIndexPath *)indexPath animated:(BOOL)animated { NSInteger backingRow = [self backingRowForIndexPath:indexPath]; - if (backingRow != NSNotFound) { + if (backingRow != NSNotFound && [_tableView.selectedRowIndexes containsIndex:backingRow]) { [_tableView deselectRow:backingRow]; } } @@ -338,13 +334,6 @@ - (NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row { - NSNumber *rowKey = @(row); - NSView *pendingView = _pendingViews[rowKey]; - if (pendingView != nil) { - [_pendingViews removeObjectForKey:rowKey]; - return pendingView; - } - return [self viewForBackingRow:row]; } @@ -387,29 +376,7 @@ - (CGFloat)tableView:(NSTableView *)tableView heightOfRow:(NSInteger)row } [_automaticRows addIndex:row]; - NSNumber *rowKey = @(row); - NSNumber *cachedHeight = _automaticHeights[rowKey]; - if (cachedHeight != nil) { - return cachedHeight.doubleValue; - } - - RCTUITableViewCell *cell = (RCTUITableViewCell *)[tableView viewAtColumn:0 row:row makeIfNecessary:NO]; - if (cell == nil) { - cell = (RCTUITableViewCell *)_pendingViews[rowKey]; - } - if (cell == nil) { - cell = (RCTUITableViewCell *)[self viewForBackingRow:row]; - _pendingViews[rowKey] = cell; - } - - NSSize previousSize = cell.frame.size; - cell.frame = NSMakeRect(0, 0, self.contentSize.width, previousSize.height); - [cell layoutSubtreeIfNeeded]; - CGFloat fittingHeight = cell.fittingSize.height; - cell.frame = NSMakeRect(cell.frame.origin.x, cell.frame.origin.y, previousSize.width, previousSize.height); - - _automaticHeights[rowKey] = @(fittingHeight); - return fittingHeight; + return tableView.rowHeight; } - (BOOL)tableView:(NSTableView *)tableView shouldSelectRow:(NSInteger)row From ca3135f55c72594946f3cd195dfbdd85a97ab482 Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Tue, 28 Jul 2026 13:19:56 -0700 Subject: [PATCH 05/14] fix(macos): constrain RCTUITableViewCell content Pin the compatibility content view to the AppKit cell so native automatic row heights can derive wrapped label intrinsic height at the actual cell width. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ReactApple/Libraries/RCTUIKit/RCTUITableView.m | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUITableView.m b/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUITableView.m index 2948c30db97d..193f08ed55aa 100644 --- a/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUITableView.m +++ b/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUITableView.m @@ -100,9 +100,15 @@ - (void)initializeWithStyle:(RCTUITableViewCellStyle)style reuseIdentifier:(NSSt _reuseIdentifier = [reuseIdentifier copy]; self.identifier = reuseIdentifier; - _contentView = [[RCTPlatformView alloc] initWithFrame:self.bounds]; - _contentView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable; + _contentView = [[RCTPlatformView alloc] initWithFrame:NSZeroRect]; + _contentView.translatesAutoresizingMaskIntoConstraints = NO; [self addSubview:_contentView]; + [NSLayoutConstraint activateConstraints:@[ + [_contentView.leadingAnchor constraintEqualToAnchor:self.leadingAnchor], + [_contentView.topAnchor constraintEqualToAnchor:self.topAnchor], + [_contentView.trailingAnchor constraintEqualToAnchor:self.trailingAnchor], + [_contentView.bottomAnchor constraintEqualToAnchor:self.bottomAnchor], + ]]; _textLabel = [[RCTUILabel alloc] initWithFrame:NSZeroRect]; _textLabel.translatesAutoresizingMaskIntoConstraints = NO; From a02d20a6058c102f37ec30b4370fd52f6115f1b5 Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Tue, 28 Jul 2026 13:21:06 -0700 Subject: [PATCH 06/14] fix(macos): preserve fixed RCTUITableView row heights Apply private cell height constraints for explicit delegate heights while leaving automatic rows to AppKit intrinsic sizing and intercell spacing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Libraries/RCTUIKit/RCTUITableView.m | 40 ++++++++++++++++--- 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUITableView.m b/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUITableView.m index 193f08ed55aa..e56a697f2d01 100644 --- a/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUITableView.m +++ b/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUITableView.m @@ -72,9 +72,12 @@ @interface RCTUITableViewCell () @property (nonatomic, readwrite, strong) RCTUILabel *textLabel; @property (nonatomic, readwrite, nullable, strong) RCTUILabel *detailTextLabel; +- (void)setFixedHeight:(nullable NSNumber *)fixedHeight; + @end @implementation RCTUITableViewCell { + NSLayoutConstraint *_fixedHeightConstraint; RCTUITableViewCellStyle _style; } @@ -176,6 +179,22 @@ - (void)layout self.detailTextLabel.preferredMaxLayoutWidth = preferredWidth; } +- (void)setFixedHeight:(NSNumber *)fixedHeight +{ + if (fixedHeight == nil) { + _fixedHeightConstraint.active = NO; + _fixedHeightConstraint = nil; + return; + } + + if (_fixedHeightConstraint == nil) { + _fixedHeightConstraint = [self.heightAnchor constraintEqualToConstant:fixedHeight.doubleValue]; + _fixedHeightConstraint.active = YES; + } else { + _fixedHeightConstraint.constant = fixedHeight.doubleValue; + } +} + @end @interface RCTUITableView () @@ -348,7 +367,10 @@ - (NSView *)viewForBackingRow:(NSInteger)backingRow RCTUITableViewSlot *slot = _slots[backingRow]; if (slot.kind == RCTUITableViewSlotKindRow) { NSIndexPath *indexPath = [NSIndexPath indexPathForRow:slot.row inSection:slot.section]; - return [self.dataSource tableView:self cellForRowAtIndexPath:indexPath]; + RCTUITableViewCell *cell = [self.dataSource tableView:self cellForRowAtIndexPath:indexPath]; + CGFloat height = [self requestedHeightForSlot:slot]; + [cell setFixedHeight:height == RCTUITableViewAutomaticDimension ? nil : @(height)]; + return cell; } NSNumber *sectionKey = @(slot.section); @@ -372,11 +394,7 @@ - (CGFloat)tableView:(NSTableView *)tableView heightOfRow:(NSInteger)row return slot.headerHeight; } - CGFloat height = tableView.rowHeight; - if ([self.delegate respondsToSelector:@selector(tableView:heightForRowAtIndexPath:)]) { - NSIndexPath *indexPath = [NSIndexPath indexPathForRow:slot.row inSection:slot.section]; - height = [self.delegate tableView:self heightForRowAtIndexPath:indexPath]; - } + CGFloat height = [self requestedHeightForSlot:slot]; if (height != RCTUITableViewAutomaticDimension) { return height; } @@ -385,6 +403,16 @@ - (CGFloat)tableView:(NSTableView *)tableView heightOfRow:(NSInteger)row return tableView.rowHeight; } +- (CGFloat)requestedHeightForSlot:(RCTUITableViewSlot *)slot +{ + if (![self.delegate respondsToSelector:@selector(tableView:heightForRowAtIndexPath:)]) { + return _tableView.rowHeight; + } + + NSIndexPath *indexPath = [NSIndexPath indexPathForRow:slot.row inSection:slot.section]; + return [self.delegate tableView:self heightForRowAtIndexPath:indexPath]; +} + - (BOOL)tableView:(NSTableView *)tableView shouldSelectRow:(NSInteger)row { RCTUITableViewSlot *slot = _slots[row]; From ac7aa639efa080eeb271a017f02bed1efdef827d Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Tue, 28 Jul 2026 13:25:29 -0700 Subject: [PATCH 07/14] fix(macos): update table label wrapping on resize Refresh preferred wrapping widths when AppKit assigns cell frames so automatic height fitting sees the current content width before layout. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ReactApple/Libraries/RCTUIKit/RCTUITableView.m | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUITableView.m b/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUITableView.m index e56a697f2d01..279027908fc0 100644 --- a/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUITableView.m +++ b/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUITableView.m @@ -170,11 +170,11 @@ - (void)prepareForReuse self.detailTextLabel.maximumNumberOfLines = 1; } -- (void)layout +- (void)setFrameSize:(NSSize)newSize { - [super layout]; + [super setFrameSize:newSize]; - CGFloat preferredWidth = MAX(NSWidth(self.contentView.bounds) - 10, 0); + CGFloat preferredWidth = MAX(newSize.width - 10, 0); self.textLabel.preferredMaxLayoutWidth = preferredWidth; self.detailTextLabel.preferredMaxLayoutWidth = preferredWidth; } From 24f605909dc7bc18227ae51ef4e6d7a7c16ad90b Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Tue, 28 Jul 2026 13:26:10 -0700 Subject: [PATCH 08/14] fix(macos): preserve RCTUITableView header heights Constrain each lazily cached header to its declared slot height so AppKit automatic row sizing keeps the explicit header contract. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../react-native/ReactApple/Libraries/RCTUIKit/RCTUITableView.m | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUITableView.m b/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUITableView.m index 279027908fc0..23a1c6b481e5 100644 --- a/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUITableView.m +++ b/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUITableView.m @@ -382,6 +382,7 @@ - (NSView *)viewForBackingRow:(NSInteger)backingRow if (headerView == nil) { headerView = [RCTPlatformView new]; } + [headerView.heightAnchor constraintEqualToConstant:slot.headerHeight].active = YES; _headerViews[sectionKey] = headerView; } return headerView; From cb3148a3c78e7b2beb789d6be4f39e3de798d974 Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Tue, 28 Jul 2026 14:45:47 -0700 Subject: [PATCH 09/14] fix(macos): preserve custom RCTUITableViewCell layout Use NSTableViewRowSizeStyleCustom so NSTableCellView does not override the compatibility label constraints assigned through textField. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../react-native/ReactApple/Libraries/RCTUIKit/RCTUITableView.m | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUITableView.m b/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUITableView.m index 23a1c6b481e5..60f7e62fd4a5 100644 --- a/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUITableView.m +++ b/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUITableView.m @@ -102,6 +102,7 @@ - (void)initializeWithStyle:(RCTUITableViewCellStyle)style reuseIdentifier:(NSSt _style = style; _reuseIdentifier = [reuseIdentifier copy]; self.identifier = reuseIdentifier; + self.rowSizeStyle = NSTableViewRowSizeStyleCustom; _contentView = [[RCTPlatformView alloc] initWithFrame:NSZeroRect]; _contentView.translatesAutoresizingMaskIntoConstraints = NO; From 994efe8759d68fca33c337e79b4d682d02d9045d Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Tue, 28 Jul 2026 14:47:57 -0700 Subject: [PATCH 10/14] fix(macos): reuse RCTUITableView header constraints Associate one primitive-owned height constraint with each header view, deactivate it between reload generations, and update/reactivate it when a delegate reuses the view. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Libraries/RCTUIKit/RCTUITableView.m | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUITableView.m b/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUITableView.m index 60f7e62fd4a5..7b04e329114b 100644 --- a/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUITableView.m +++ b/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUITableView.m @@ -11,7 +11,11 @@ #if TARGET_OS_OSX +#import + const CGFloat RCTUITableViewAutomaticDimension = -1.0; +static NSString *const RCTUITableViewHeaderHeightConstraintIdentifier = @"RCTUITableViewHeaderHeight"; +static char RCTUITableViewHeaderHeightConstraintKey; typedef NS_ENUM(NSInteger, RCTUITableViewSlotKind) { RCTUITableViewSlotKindHeader, @@ -275,6 +279,11 @@ - (void)setFrameSize:(NSSize)newSize - (void)reloadData { + for (RCTPlatformView *headerView in _headerViews.allValues) { + NSLayoutConstraint *heightConstraint = + objc_getAssociatedObject(headerView, &RCTUITableViewHeaderHeightConstraintKey); + heightConstraint.active = NO; + } [_headerViews removeAllObjects]; [_automaticRows removeAllIndexes]; @@ -383,7 +392,21 @@ - (NSView *)viewForBackingRow:(NSInteger)backingRow if (headerView == nil) { headerView = [RCTPlatformView new]; } - [headerView.heightAnchor constraintEqualToConstant:slot.headerHeight].active = YES; + headerView.translatesAutoresizingMaskIntoConstraints = NO; + NSLayoutConstraint *heightConstraint = + objc_getAssociatedObject(headerView, &RCTUITableViewHeaderHeightConstraintKey); + if (heightConstraint == nil) { + heightConstraint = [headerView.heightAnchor constraintEqualToConstant:slot.headerHeight]; + heightConstraint.identifier = RCTUITableViewHeaderHeightConstraintIdentifier; + objc_setAssociatedObject( + headerView, + &RCTUITableViewHeaderHeightConstraintKey, + heightConstraint, + OBJC_ASSOCIATION_RETAIN_NONATOMIC); + } else { + heightConstraint.constant = slot.headerHeight; + } + heightConstraint.active = YES; _headerViews[sectionKey] = headerView; } return headerView; From e61b81c8c5529d974d8933efa6570f93b5586981 Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Tue, 28 Jul 2026 14:50:22 -0700 Subject: [PATCH 11/14] fix(macos): preserve table cell content insets Keep 5pt vertical containment for subtitle cells and the RedBox stack carve-out so AppKit cannot clip glyph tops at row boundaries. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ReactApple/Libraries/RCTUIKit/RCTUITableView.m | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUITableView.m b/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUITableView.m index 7b04e329114b..808668e26979 100644 --- a/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUITableView.m +++ b/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUITableView.m @@ -130,12 +130,12 @@ - (void)initializeWithStyle:(RCTUITableViewCellStyle)style reuseIdentifier:(NSSt [NSLayoutConstraint activateConstraints:@[ [_textLabel.leadingAnchor constraintEqualToAnchor:_contentView.leadingAnchor constant:5], - [_textLabel.topAnchor constraintEqualToAnchor:_contentView.topAnchor], + [_textLabel.topAnchor constraintEqualToAnchor:_contentView.topAnchor constant:5], [_textLabel.trailingAnchor constraintEqualToAnchor:_contentView.trailingAnchor constant:-5], [_detailTextLabel.leadingAnchor constraintEqualToAnchor:_contentView.leadingAnchor constant:5], [_detailTextLabel.topAnchor constraintEqualToAnchor:_textLabel.bottomAnchor], [_detailTextLabel.trailingAnchor constraintEqualToAnchor:_contentView.trailingAnchor constant:-5], - [_detailTextLabel.bottomAnchor constraintEqualToAnchor:_contentView.bottomAnchor], + [_detailTextLabel.bottomAnchor constraintEqualToAnchor:_contentView.bottomAnchor constant:-5], ]]; } else { [NSLayoutConstraint activateConstraints:@[ From b1c27df1d2fd048dce6e9178767e87174f6993c9 Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Fri, 24 Jul 2026 19:21:20 -0700 Subject: [PATCH 12/14] 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 3f6a63e67a29..4fa60a55adcb 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 a7a75b93c1f1..ba3c39798ce1 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 56fb86f387b8..7339d6dc010b 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 4fa0cd0b610e..1be4b3b7cc8c 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 7976bf19585a184960faab5d217e0ffb69b904a0 Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Mon, 27 Jul 2026 03:39:54 -0700 Subject: [PATCH 13/14] 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 9f425218f108..6e0a2b617c20 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 c631f7457bf5545eed8344f749c2560820c9e705 Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Tue, 28 Jul 2026 21:09:08 -0700 Subject: [PATCH 14/14] 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 7339d6dc010b..905e9398487f 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 1be4b3b7cc8c..f17591f1e984 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]