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]; 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..905e9398487f 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 4fa0cd0b610e..f17591f1e984 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 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 eccebc648400..7d51cf2dfe76 100644 --- a/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUIKit.h +++ b/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUIKit.h @@ -16,6 +16,8 @@ #import "RCTUIView.h" #import "RCTUIScrollView.h" #import "RCTUISlider.h" +#import "RCTUITableView.h" +#import "RCTUIButton.h" #import "RCTUILabel.h" #import "RCTUISwitch.h" #import "RCTUIActivityIndicatorView.h" 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 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..1abaf797f52b --- /dev/null +++ b/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUITableView.h @@ -0,0 +1,111 @@ +/* + * 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; + +- (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..808668e26979 --- /dev/null +++ b/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUITableView.m @@ -0,0 +1,457 @@ +/* + * 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 + +#import + +const CGFloat RCTUITableViewAutomaticDimension = -1.0; +static NSString *const RCTUITableViewHeaderHeightConstraintIdentifier = @"RCTUITableViewHeaderHeight"; +static char RCTUITableViewHeaderHeightConstraintKey; + +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; + +- (void)setFixedHeight:(nullable NSNumber *)fixedHeight; + +@end + +@implementation RCTUITableViewCell { + NSLayoutConstraint *_fixedHeightConstraint; + 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; + self.rowSizeStyle = NSTableViewRowSizeStyleCustom; + + _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; + [_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 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 constant:-5], + ]]; + } 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; +} + +- (void)setFrameSize:(NSSize)newSize +{ + [super setFrameSize:newSize]; + + CGFloat preferredWidth = MAX(newSize.width - 10, 0); + self.textLabel.preferredMaxLayoutWidth = preferredWidth; + 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 () +@end + +@implementation RCTUITableView { + NSTableView *_tableView; + NSArray *_slots; + NSMutableDictionary *_headerViews; + NSMutableIndexSet *_automaticRows; + CGFloat _lastContentWidth; +} + +- (instancetype)initWithFrame:(NSRect)frameRect +{ + if (self = [super initWithFrame:frameRect]) { + self.drawsBackground = NO; + + _slots = @[]; + _headerViews = [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.usesAutomaticRowHeights = YES; + _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; + } +} + +- (void)setFrameSize:(NSSize)newSize +{ + [super setFrameSize:newSize]; + + CGFloat contentWidth = self.contentSize.width; + if (_lastContentWidth != contentWidth) { + _lastContentWidth = contentWidth; + if (_automaticRows.count > 0) { + [_tableView noteHeightOfRowsWithIndexesChanged:_automaticRows]; + } + } +} + +- (void)reloadData +{ + for (RCTPlatformView *headerView in _headerViews.allValues) { + NSLayoutConstraint *heightConstraint = + objc_getAssociatedObject(headerView, &RCTUITableViewHeaderHeightConstraintKey); + heightConstraint.active = NO; + } + [_headerViews 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 +{ + return [_tableView makeViewWithIdentifier:identifier owner:self]; +} + +- (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.selectedRowIndexes containsIndex:backingRow]) { + [_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 +{ + 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]; + 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); + 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]; + } + 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; +} + +- (CGFloat)tableView:(NSTableView *)tableView heightOfRow:(NSInteger)row +{ + RCTUITableViewSlot *slot = _slots[row]; + if (slot.kind == RCTUITableViewSlotKindHeader) { + return slot.headerHeight; + } + + CGFloat height = [self requestedHeightForSlot:slot]; + if (height != RCTUITableViewAutomaticDimension) { + return height; + } + + [_automaticRows addIndex: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]; + 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