diff --git a/packages/react-native/React/CoreModules/RCTRedBox.mm b/packages/react-native/React/CoreModules/RCTRedBox.mm index 9f425218f10..6e0a2b617c2 100644 --- a/packages/react-native/React/CoreModules/RCTRedBox.mm +++ b/packages/react-native/React/CoreModules/RCTRedBox.mm @@ -21,9 +21,7 @@ #import "CoreModulesPlugins.h" #import "RCTRedBox+Internal.h" -#if !TARGET_OS_OSX // [macOS] #import "RCTRedBox2Controller+Internal.h" -#endif // [macOS] #import "RCTRedBoxController+Internal.h" #if RCT_DEV_MENU @@ -197,11 +195,9 @@ - (void)showErrorMessage:(NSString *)message errorInfo = [self _customizeError:errorInfo]; if (self->_controller == nullptr) { - if (!TARGET_OS_OSX && facebook::react::ReactNativeFeatureFlags::redBoxV2IOS()) { // [macOS] -#if !TARGET_OS_OSX // [macOS] + if (facebook::react::ReactNativeFeatureFlags::redBoxV2IOS()) { self->_controller = [[RCTRedBox2Controller alloc] initWithCustomButtonTitles:self->_customButtonTitles customButtonHandlers:self->_customButtonHandlers]; -#endif // [macOS] } else { self->_controller = [[RCTRedBoxController alloc] initWithCustomButtonTitles:self->_customButtonTitles customButtonHandlers:self->_customButtonHandlers]; diff --git a/packages/react-native/React/CoreModules/RCTRedBox2AnsiParser+Internal.h b/packages/react-native/React/CoreModules/RCTRedBox2AnsiParser+Internal.h index 3f6a63e67a2..4fa60a55adc 100644 --- a/packages/react-native/React/CoreModules/RCTRedBox2AnsiParser+Internal.h +++ b/packages/react-native/React/CoreModules/RCTRedBox2AnsiParser+Internal.h @@ -5,10 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import - -#if !TARGET_OS_OSX // [macOS] -#import +#import // [macOS] /** * Parses ANSI escape sequences in text and produces an NSAttributedString @@ -18,10 +15,14 @@ */ @interface RCTRedBox2AnsiParser : NSObject +#if !TARGET_OS_OSX // [macOS] + (NSAttributedString *)attributedStringFromAnsiText:(NSString *)text baseFont:(UIFont *)font baseColor:(UIColor *)color; +#else // [macOS ++ (NSAttributedString *)attributedStringFromAnsiText:(NSString *)text + baseFont:(UIFont *)font + baseColor:(RCTPlatformColor *)color; +#endif // macOS] @end - -#endif // [macOS] diff --git a/packages/react-native/React/CoreModules/RCTRedBox2AnsiParser.mm b/packages/react-native/React/CoreModules/RCTRedBox2AnsiParser.mm index a7a75b93c1f..ba3c39798ce 100644 --- a/packages/react-native/React/CoreModules/RCTRedBox2AnsiParser.mm +++ b/packages/react-native/React/CoreModules/RCTRedBox2AnsiParser.mm @@ -10,19 +10,32 @@ #import #import -#if RCT_DEV_MENU && !TARGET_OS_OSX // [macOS] +#if RCT_DEV_MENU using facebook::react::unstable_redbox::AnsiColor; using facebook::react::unstable_redbox::parseAnsi; +#if !TARGET_OS_OSX // [macOS] static UIColor *RCTUIColorFromAnsiColor(const AnsiColor &c) { return [UIColor colorWithRed:c.r / 255.0 green:c.g / 255.0 blue:c.b / 255.0 alpha:1.0]; } +#else // [macOS +static RCTPlatformColor *RCTUIColorFromAnsiColor(const AnsiColor &c) +{ + return [RCTPlatformColor colorWithRed:c.r / 255.0 green:c.g / 255.0 blue:c.b / 255.0 alpha:1.0]; +} +#endif // macOS] @implementation RCTRedBox2AnsiParser +#if !TARGET_OS_OSX // [macOS] + (NSAttributedString *)attributedStringFromAnsiText:(NSString *)text baseFont:(UIFont *)font baseColor:(UIColor *)color +#else // [macOS ++ (NSAttributedString *)attributedStringFromAnsiText:(NSString *)text + baseFont:(UIFont *)font + baseColor:(RCTPlatformColor *)color +#endif // macOS] { if (text == nil) { return [[NSAttributedString alloc] init]; diff --git a/packages/react-native/React/CoreModules/RCTRedBox2Controller+Internal.h b/packages/react-native/React/CoreModules/RCTRedBox2Controller+Internal.h index 56fb86f387b..905e9398487 100644 --- a/packages/react-native/React/CoreModules/RCTRedBox2Controller+Internal.h +++ b/packages/react-native/React/CoreModules/RCTRedBox2Controller+Internal.h @@ -6,14 +6,16 @@ */ #import +#import // [macOS] #import "RCTRedBox+Internal.h" -#if RCT_DEV_MENU && !TARGET_OS_OSX // [macOS] +#if RCT_DEV_MENU typedef void (^RCTRedBox2ButtonPressHandler)(void); -@interface RCTRedBox2Controller : UIViewController +@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 4fa0cd0b610..f17591f1e98 100644 --- a/packages/react-native/React/CoreModules/RCTRedBox2Controller.mm +++ b/packages/react-native/React/CoreModules/RCTRedBox2Controller.mm @@ -22,25 +22,27 @@ // @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) -static UIColor *RCTRedBox2BackgroundColor() +// [macOS +static RCTUIColor *RCTRedBox2BackgroundColor() { - return [UIColor colorWithRed:51.0 / 255 green:51.0 / 255 blue:51.0 / 255 alpha:1.0]; + return [RCTUIColor colorWithRed:51.0 / 255 green:51.0 / 255 blue:51.0 / 255 alpha:1.0]; } -static UIColor *RCTRedBox2ErrorColor() +static RCTUIColor *RCTRedBox2ErrorColor() { - return [UIColor colorWithRed:243.0 / 255 green:83.0 / 255 blue:105.0 / 255 alpha:1.0]; + return [RCTUIColor colorWithRed:243.0 / 255 green:83.0 / 255 blue:105.0 / 255 alpha:1.0]; } -static UIColor *RCTRedBox2TextColor(CGFloat opacity) +static RCTUIColor *RCTRedBox2TextColor(CGFloat opacity) { - return [UIColor colorWithWhite:1.0 alpha:opacity]; + return [RCTUIColor colorWithWhite:1.0 alpha:opacity]; } +// macOS] enum class Section : uint8_t { Message, CodeFrame, CallStack, kMaxValue }; static constexpr size_t kSectionCount = static_cast(Section::kMaxValue); @@ -52,9 +54,11 @@ static const NSTimeInterval kAutoRetryInterval = 20.0; @implementation RCTRedBox2Controller { - UITableView *_stackTraceTableView; - UILabel *_headerTitleLabel; - UILabel *_errorCategoryLabel; + // [macOS + RCTUITableView *_stackTraceTableView; + RCTUILabel *_headerTitleLabel; + RCTUILabel *_errorCategoryLabel; + // macOS] NSString *_lastErrorMessage; NSArray *_lastStackTrace; NSArray *_customButtonTitles; @@ -64,7 +68,7 @@ @implementation RCTRedBox2Controller { std::array _sectionStates; NSTimer *_autoRetryTimer; NSInteger _autoRetryCountdown; - UIButton *_reloadButton; + RCTUIButton *_reloadButton; // [macOS] NSString *_reloadBaseText; RCTRedBoxHMRClient *_hmrClient; } @@ -77,7 +81,9 @@ - (instancetype)initWithCustomButtonTitles:(NSArray *)customButtonTi _lastErrorCookie = -1; _customButtonTitles = customButtonTitles; _customButtonHandlers = customButtonHandlers; +#if !TARGET_OS_OSX // [macOS] self.modalPresentationStyle = UIModalPresentationFullScreen; +#endif // [macOS] } return self; } @@ -85,25 +91,32 @@ - (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) - UIView *headerBar = [self createHeaderBar]; - - // Footer button bar - UIView *footerBar = [self createFooterBar]; + RCTPlatformView *headerBar = [self createHeaderBar]; // [macOS] + RCTPlatformView *footerBar = [self createFooterBar]; // [macOS] // Stack trace table - _stackTraceTableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain]; - _stackTraceTableView.translatesAutoresizingMaskIntoConstraints = NO; - _stackTraceTableView.delegate = self; - _stackTraceTableView.dataSource = self; +#if !TARGET_OS_OSX // [macOS] + _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:@[ @@ -116,30 +129,40 @@ - (void)viewDidLoad #pragma mark - Header Bar -- (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], +#else // [macOS + [_headerTitleLabel.topAnchor constraintEqualToAnchor:headerContainer.topAnchor constant:12], +#endif // macOS] ]]; return headerContainer; @@ -147,102 +170,107 @@ - (UIView *)createHeaderBar #pragma mark - Footer Bar -- (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(); - +#else // [macOS + [dismissButton setKeyEquivalent:@"\e"]; + [_reloadButton setKeyEquivalent:@"r"]; + [_reloadButton setKeyEquivalentModifierMask:NSEventModifierFlagCommand]; + [copyButton setKeyEquivalent:@"c"]; + [copyButton setKeyEquivalentModifierMask:NSEventModifierFlagOption | NSEventModifierFlagCommand]; + + NSStackView *buttonStackView = [[NSStackView alloc] init]; + 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++) { - UIButton *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 = [UIColor 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 (UIButton *btn in buttonStackView.arrangedSubviews) { - [btn.heightAnchor constraintEqualToConstant:buttonHeight].active = YES; + for (RCTPlatformView *button in buttonStackView.arrangedSubviews) { // [macOS] + [button.heightAnchor constraintEqualToConstant:buttonHeight].active = YES; } return buttonStackView; } -- (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]; + [button setTitle:title forState:RCTUIControlStateNormal]; // [macOS] + [button setTitleColor:[RCTUIColor whiteColor] forState:RCTUIControlStateNormal]; // [macOS] + [button setTitleColor:RCTRedBox2TextColor(0.5) forState:RCTUIControlStateHighlighted]; // [macOS] return button; } -- (UIButton *)footerButton:(NSString *)title - accessibilityIdentifier:(NSString *)accessibilityIdentifier - handler:(RCTRedBox2ButtonPressHandler)handler +- (RCTUIButton *)footerButton:(NSString *)title // [macOS] + accessibilityIdentifier:(NSString *)accessibilityIdentifier + handler:(RCTRedBox2ButtonPressHandler)handler { - UIButton *button = [self styledButton:title accessibilityIdentifier:accessibilityIdentifier]; - [button addAction:[UIAction actionWithHandler:^(__unused UIAction *action) { - handler(); - }] - forControlEvents:UIControlEventTouchUpInside]; + RCTUIButton *button = [self styledButton:title accessibilityIdentifier:accessibilityIdentifier]; // [macOS] + [button rct_setPrimaryAction:[RCTUIAction actionWithHandler:handler]]; // [macOS] return button; } - (CGFloat)bottomSafeViewHeight { -#if TARGET_OS_MACCATALYST +#if TARGET_OS_MACCATALYST || TARGET_OS_OSX // [macOS] return 0; #else return RCTKeyWindow().safeAreaInsets.bottom; @@ -289,15 +317,19 @@ - (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) - _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]; + atScrollPosition:RCTUITableViewScrollPositionTop + animated:NO]; // [macOS] [self startAutoRetryIfApplicable]; [self _startHMRClient]; @@ -307,7 +339,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 +405,7 @@ - (void)stopAutoRetry [_autoRetryTimer invalidate]; _autoRetryTimer = nil; if (_reloadButton) { - [_reloadButton setTitle:_reloadBaseText forState:UIControlStateNormal]; + [_reloadButton setTitle:_reloadBaseText forState:RCTUIControlStateNormal]; // [macOS] } } @@ -385,7 +423,7 @@ - (void)autoRetryTick - (void)updateReloadButtonTitle { NSString *title = [NSString stringWithFormat:@"%@ (%lds)", _reloadBaseText, (long)_autoRetryCountdown]; - [_reloadButton setTitle:title forState:UIControlStateNormal]; + [_reloadButton setTitle:title forState:RCTUIControlStateNormal]; // [macOS] } - (void)copyStack @@ -405,10 +443,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,12 +516,12 @@ - (NSString *)displayMessage #pragma mark - TableView DataSource & Delegate -- (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); @@ -485,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]; @@ -544,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)"; @@ -577,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; @@ -624,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 @@ -645,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], @@ -655,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], @@ -665,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: @@ -687,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]; @@ -708,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: @@ -721,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; @@ -736,9 +813,9 @@ - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath } [tableView deselectRowAtIndexPath:indexPath animated:YES]; } - #pragma mark - Key Commands +#if !TARGET_OS_OSX // [macOS] - (NSArray *)keyCommands { return @[ @@ -758,6 +835,7 @@ - (BOOL)canBecomeFirstResponder { return YES; } +#endif // [macOS] @end diff --git a/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUIButton.h b/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUIButton.h new file mode 100644 index 00000000000..4f97eacf107 --- /dev/null +++ b/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUIButton.h @@ -0,0 +1,73 @@ +/* + * Copyright (c) Microsoft Corporation. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +// [macOS] + +#pragma once + +#include + +#import "RCTUIKitCompat.h" + +#if !TARGET_OS_OSX +#import +#else +#import +#endif + +NS_ASSUME_NONNULL_BEGIN + +#if !TARGET_OS_OSX + +@compatibility_alias RCTUIButton UIButton; +#define RCTUIControlStateNormal UIControlStateNormal +#define RCTUIControlStateHighlighted UIControlStateHighlighted +typedef UIControlState RCTUIControlState; + +#else // TARGET_OS_OSX [ + +typedef NS_OPTIONS(NSUInteger, RCTUIControlState) { + RCTUIControlStateNormal = 0, + RCTUIControlStateHighlighted = 1 << 0, +}; + +@interface RCTUIButtonTitleProxy : NSObject + +@property (nonatomic, strong, nullable) UIFont *font; +@property (nonatomic, assign) NSLineBreakMode lineBreakMode; +@property (nonatomic, assign) NSTextAlignment textAlignment; + +@end + +@interface RCTUIButton : NSButton + +@property (nonatomic, readonly) RCTUIButtonTitleProxy *titleLabel; +@property (nonatomic, copy, nullable) RCTUIColor *backgroundColor; + +- (void)setTitle:(nullable NSString *)title forState:(RCTUIControlState)state; +- (void)setTitleColor:(nullable RCTUIColor *)color forState:(RCTUIControlState)state; + +@end + +#endif // ] TARGET_OS_OSX + +typedef void (^RCTUIActionHandler)(void); + +@interface RCTUIAction : NSObject + ++ (instancetype)actionWithHandler:(RCTUIActionHandler)handler; +@property (nonatomic, readonly, copy) RCTUIActionHandler handler; + +@end + +@interface RCTUIButton (RCTUIAction) + +- (void)rct_setPrimaryAction:(RCTUIAction *)action; + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUIButton.m b/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUIButton.m new file mode 100644 index 00000000000..d01c39f7fb4 --- /dev/null +++ b/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUIButton.m @@ -0,0 +1,197 @@ +/* + * Copyright (c) Microsoft Corporation. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +// [macOS] + +#import "RCTUIButton.h" + +#import + +@interface RCTUIAction () + +- (instancetype)initWithHandler:(RCTUIActionHandler)handler; +- (void)invoke; + +@end + +@implementation RCTUIAction + ++ (instancetype)actionWithHandler:(RCTUIActionHandler)handler +{ + return [[self alloc] initWithHandler:handler]; +} + +- (instancetype)initWithHandler:(RCTUIActionHandler)handler +{ + if (self = [super init]) { + _handler = [handler copy]; + } + return self; +} + +- (void)invoke +{ + self.handler(); +} + +@end + +#if TARGET_OS_OSX + +@class RCTUIButton; + +@interface RCTUIButtonTitleProxy () + +@property (nonatomic, weak) RCTUIButton *button; + +- (instancetype)initWithButton:(RCTUIButton *)button; + +@end + +@interface RCTUIButton () + +- (void)updateAttributedTitles; + +@end + +@implementation RCTUIButtonTitleProxy + +- (instancetype)initWithButton:(RCTUIButton *)button +{ + if (self = [super init]) { + _button = button; + _font = button.font; + _lineBreakMode = NSLineBreakByClipping; + _textAlignment = NSTextAlignmentNatural; + } + return self; +} + +- (void)setFont:(UIFont *)font +{ + _font = font; + [self.button updateAttributedTitles]; +} + +- (void)setLineBreakMode:(NSLineBreakMode)lineBreakMode +{ + _lineBreakMode = lineBreakMode; + [self.button updateAttributedTitles]; +} + +- (void)setTextAlignment:(NSTextAlignment)textAlignment +{ + _textAlignment = textAlignment; + [self.button updateAttributedTitles]; +} + +@end + +@implementation RCTUIButton { + NSString *_normalTitle; + NSString *_highlightedTitle; + RCTUIColor *_normalTitleColor; + RCTUIColor *_highlightedTitleColor; +} + +- (instancetype)initWithFrame:(NSRect)frameRect +{ + if (self = [super initWithFrame:frameRect]) { + _titleLabel = [[RCTUIButtonTitleProxy alloc] initWithButton:self]; + } + return self; +} + +- (instancetype)initWithCoder:(NSCoder *)coder +{ + if (self = [super initWithCoder:coder]) { + _titleLabel = [[RCTUIButtonTitleProxy alloc] initWithButton:self]; + } + return self; +} + +- (void)setTitle:(NSString *)title forState:(RCTUIControlState)state +{ + if (state == RCTUIControlStateHighlighted) { + _highlightedTitle = [title copy]; + } else { + _normalTitle = [title copy]; + } + [self updateAttributedTitles]; +} + +- (void)setTitleColor:(RCTUIColor *)color forState:(RCTUIControlState)state +{ + if (state == RCTUIControlStateHighlighted) { + _highlightedTitleColor = [color copy]; + } else { + _normalTitleColor = [color copy]; + } + [self updateAttributedTitles]; +} + +- (void)setBackgroundColor:(RCTUIColor *)backgroundColor +{ + _backgroundColor = [backgroundColor copy]; + self.wantsLayer = YES; + self.layer.backgroundColor = backgroundColor.CGColor; + self.bordered = NO; +} + +- (void)updateAttributedTitles +{ + self.attributedTitle = [self attributedTitleForTitle:_normalTitle ?: @"" + color:_normalTitleColor]; + self.attributedAlternateTitle = [self attributedTitleForTitle:_highlightedTitle ?: _normalTitle ?: @"" + color:_highlightedTitleColor ?: _normalTitleColor]; +} + +- (NSAttributedString *)attributedTitleForTitle:(NSString *)title color:(RCTUIColor *)color +{ + NSMutableDictionary *attributes = [NSMutableDictionary new]; + if (color != nil) { + attributes[NSForegroundColorAttributeName] = color; + } + if (self.titleLabel.font != nil) { + attributes[NSFontAttributeName] = self.titleLabel.font; + } + + NSMutableParagraphStyle *paragraphStyle = [NSMutableParagraphStyle new]; + paragraphStyle.lineBreakMode = self.titleLabel.lineBreakMode; + paragraphStyle.alignment = self.titleLabel.textAlignment; + attributes[NSParagraphStyleAttributeName] = paragraphStyle; + + return [[NSAttributedString alloc] initWithString:title attributes:attributes]; +} + +@end + +#endif + +@implementation RCTUIButton (RCTUIAction) + +- (void)rct_setPrimaryAction:(RCTUIAction *)action +{ +#if !TARGET_OS_OSX + RCTUIAction *previousAction = objc_getAssociatedObject(self, @selector(rct_setPrimaryAction:)); + if (previousAction != nil) { + [self removeTarget:previousAction action:@selector(invoke) forControlEvents:UIControlEventTouchUpInside]; + } +#endif + + objc_setAssociatedObject( + self, @selector(rct_setPrimaryAction:), action, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + +#if !TARGET_OS_OSX + [self addTarget:action action:@selector(invoke) forControlEvents:UIControlEventTouchUpInside]; +#else + self.target = action; + self.action = @selector(invoke); +#endif +} + +@end diff --git a/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUIKit.h b/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUIKit.h index e1a2f0ae022..7d51cf2dfe7 100644 --- a/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUIKit.h +++ b/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUIKit.h @@ -17,6 +17,7 @@ #import "RCTUIScrollView.h" #import "RCTUISlider.h" #import "RCTUITableView.h" +#import "RCTUIButton.h" #import "RCTUILabel.h" #import "RCTUISwitch.h" #import "RCTUIActivityIndicatorView.h"