diff --git a/packages/react-native/React/CoreModules/RCTRedBox.mm b/packages/react-native/React/CoreModules/RCTRedBox.mm index 0531ab50eb7a..1967ab399709 100644 --- a/packages/react-native/React/CoreModules/RCTRedBox.mm +++ b/packages/react-native/React/CoreModules/RCTRedBox.mm @@ -15,6 +15,7 @@ #import #import #import +#import // [macOS] #import #import @@ -65,7 +66,7 @@ - (void)rct_callBlock } #if !TARGET_OS_OSX // [macOS] -- (void)rct_addBlock:(RCTRedBoxButtonPressHandler)handler forControlEvents:(UIControlEvents)controlEvents; +- (void)rct_addBlock:(RCTRedBoxButtonPressHandler)handler forControlEvents:(UIControlEvents)controlEvents #else // [macOS - (void)rct_addBlock:(RCTRedBoxButtonPressHandler)handler #endif // macOS] @@ -89,20 +90,12 @@ - (void)loadExtraDataViewController; @end -#if !TARGET_OS_OSX // [macOS] -@interface RCTRedBoxController : UIViewController -#else // [macOS -@interface RCTRedBoxController : NSViewController -#endif // macOS] +@interface RCTRedBoxController : RCTPlatformViewController // [macOS] @property (nonatomic, weak) id actionDelegate; @end @implementation RCTRedBoxController { -#if !TARGET_OS_OSX // [macOS] - UITableView *_stackTraceTableView; -#else // [macOS - NSTableView *_stackTraceTableView; -#endif // macOS] + RCTUITableView *_stackTraceTableView; // [macOS] NSString *_lastErrorMessage; NSArray *_lastStackTrace; NSArray *_customButtonTitles; @@ -138,38 +131,21 @@ - (void)viewDidLoad detailsFrame.size.height -= buttonHeight + (double)[self bottomSafeViewHeight]; #if !TARGET_OS_OSX // [macOS] - _stackTraceTableView = [[UITableView alloc] initWithFrame:detailsFrame style:UITableViewStylePlain]; + _stackTraceTableView = [[RCTUITableView alloc] initWithFrame:detailsFrame style:UITableViewStylePlain]; // [macOS] _stackTraceTableView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; - _stackTraceTableView.delegate = self; - _stackTraceTableView.dataSource = self; _stackTraceTableView.backgroundColor = [UIColor clearColor]; _stackTraceTableView.separatorColor = [UIColor colorWithWhite:1 alpha:0.3]; _stackTraceTableView.separatorStyle = UITableViewCellSeparatorStyleNone; _stackTraceTableView.indicatorStyle = UIScrollViewIndicatorStyleWhite; - [self.view addSubview:_stackTraceTableView]; #else // [macOS - NSScrollView *scrollView = [[NSScrollView alloc] initWithFrame:NSZeroRect]; - scrollView.translatesAutoresizingMaskIntoConstraints = NO; - scrollView.autoresizesSubviews = YES; - scrollView.drawsBackground = NO; - - _stackTraceTableView = [[NSTableView alloc] initWithFrame:NSZeroRect]; + _stackTraceTableView = [[RCTUITableView alloc] initWithFrame:NSZeroRect]; _stackTraceTableView.translatesAutoresizingMaskIntoConstraints = NO; - _stackTraceTableView.dataSource = self; - _stackTraceTableView.delegate = self; - _stackTraceTableView.headerView = nil; - _stackTraceTableView.allowsColumnReordering = NO; - _stackTraceTableView.allowsColumnResizing = NO; - _stackTraceTableView.columnAutoresizingStyle = NSTableViewFirstColumnOnlyAutoresizingStyle; - _stackTraceTableView.backgroundColor = [NSColor clearColor]; - _stackTraceTableView.allowsTypeSelect = NO; - - NSTableColumn *tableColumn = [[NSTableColumn alloc] initWithIdentifier:@"info"]; - [_stackTraceTableView addTableColumn:tableColumn]; - - scrollView.documentView = _stackTraceTableView; - [self.view addSubview:scrollView]; #endif // macOS] + // [macOS hoisted out of the platform branches so macOS shares the same setup + _stackTraceTableView.delegate = self; + _stackTraceTableView.dataSource = self; + [self.view addSubview:_stackTraceTableView]; + // macOS] #if TARGET_OS_SIMULATOR || TARGET_OS_MACCATALYST || TARGET_OS_OSX // [macOS] NSString *reloadText = @"Reload\n(\u2318R)"; @@ -270,7 +246,7 @@ - (void)viewDidLoad selector:nil block:_customButtonHandlers[i]]; #else // [macOS - NSButton *button = [self redBoxButton:_customButtonTitles[i] + NSButton *button = [self redBoxButton:_customButtonTitles[i] accessibilityIdentifier:@"" selector:nil block:_customButtonHandlers[i]]; @@ -299,10 +275,10 @@ - (void)viewDidLoad #if TARGET_OS_OSX // [macOS [NSLayoutConstraint activateConstraints:@[ - [[scrollView leadingAnchor] constraintEqualToAnchor:[[self view] leadingAnchor]], - [[scrollView topAnchor] constraintEqualToAnchor:[[self view] topAnchor]], - [[scrollView trailingAnchor] constraintEqualToAnchor:[[self view] trailingAnchor]], - [[scrollView bottomAnchor] constraintEqualToAnchor:[topBorder topAnchor]], + [[_stackTraceTableView leadingAnchor] constraintEqualToAnchor:[[self view] leadingAnchor]], + [[_stackTraceTableView topAnchor] constraintEqualToAnchor:[[self view] topAnchor]], + [[_stackTraceTableView trailingAnchor] constraintEqualToAnchor:[[self view] trailingAnchor]], + [[_stackTraceTableView bottomAnchor] constraintEqualToAnchor:[topBorder topAnchor]], ]]; #endif // macOS] } @@ -339,7 +315,7 @@ - (NSButton *)redBoxButton:(NSString *)title { NSButton *button = [[NSButton alloc] initWithFrame:NSZeroRect]; button.translatesAutoresizingMaskIntoConstraints = NO; - button.accessibilityIdentifier = @"accessibilityIdentifier"; + button.accessibilityIdentifier = accessibilityIdentifier; button.bordered = NO; NSAttributedString *attributedTitle = [[NSAttributedString alloc] initWithString:title @@ -401,14 +377,15 @@ - (void)showErrorMessage:(NSString *)message [_stackTraceTableView reloadData]; if (!isRootViewControllerPresented) { + if (_lastErrorMessage != nil) { // [macOS + [_stackTraceTableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0] + atScrollPosition:RCTUITableViewScrollPositionTop + animated:NO]; + } // macOS] #if !TARGET_OS_OSX // [macOS] - [_stackTraceTableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0] - atScrollPosition:UITableViewScrollPositionTop - animated:NO]; [RCTKeyWindow().rootViewController presentViewController:self animated:YES completion:nil]; #else // [macOS - [_stackTraceTableView scrollRowToVisible:0]; - [[RCTKeyWindow() contentViewController] presentViewControllerAsSheet:self]; + [[RCTKeyWindow() contentViewController] presentViewControllerAsSheet:self]; #endif // macOS] } } @@ -481,200 +458,125 @@ - (NSString *)formatFrameSource:(RCTJSStackFrame *)stackFrame #pragma mark - TableView -#if !TARGET_OS_OSX // [macOS] -- (NSInteger)numberOfSectionsInTableView:(__unused UITableView *)tableView +- (NSInteger)numberOfSectionsInTableView:(__unused RCTUITableView *)tableView // [macOS] { return 2; } -- (NSInteger)tableView:(__unused UITableView *)tableView numberOfRowsInSection:(NSInteger)section +- (NSInteger)tableView:(__unused RCTUITableView *)tableView numberOfRowsInSection:(NSInteger)section // [macOS] { - return section == 0 ? 1 : _lastStackTrace.count; + return section == 0 ? (_lastErrorMessage != nil) : _lastStackTrace.count; // [macOS] } -#else // [macOS -- (NSInteger)numberOfRowsInTableView:(__unused NSTableView *)tableView -{ - return (_lastErrorMessage != nil) + _lastStackTrace.count; -} -#endif // macOS] -#if !TARGET_OS_OSX // [macOS] -- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath +- (RCTUITableViewCell *)tableView:(RCTUITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath // [macOS] { if (indexPath.section == 0) { - UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"msg-cell"]; + RCTUITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"msg-cell"]; // [macOS] return [self reuseCell:cell forErrorMessage:_lastErrorMessage]; } - 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]; } -#else // [macOS -- (nullable NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(nullable NSTableColumn *)tableColumn row:(NSInteger)row -{ - if (row == 0) { - NSTableCellView *cell = [tableView makeViewWithIdentifier:@"msg-cell" owner:nil]; - return [self reuseCell:cell forErrorMessage:_lastErrorMessage]; - } - NSTableCellView *cell = [tableView makeViewWithIdentifier:@"cell" owner:nil]; - NSUInteger index = row - 1; - RCTJSStackFrame *stackFrame = _lastStackTrace[index]; - return [self reuseCell:cell forStackFrame:stackFrame]; - -} -#endif // macOS] -#if !TARGET_OS_OSX -- (UITableViewCell *)reuseCell:(UITableViewCell *)cell forErrorMessage:(NSString *)message +- (RCTUITableViewCell *)reuseCell:(RCTUITableViewCell *)cell forErrorMessage:(NSString *)message // [macOS] { if (!cell) { - cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"msg-cell"]; - cell.textLabel.accessibilityIdentifier = @"redbox-error"; - cell.textLabel.textColor = [UIColor whiteColor]; + cell = [[RCTUITableViewCell alloc] initWithStyle:RCTUITableViewCellStyleDefault + reuseIdentifier:@"msg-cell"]; // [macOS] + cell.textLabel.textColor = [RCTUIColor whiteColor]; // [macOS] // Prefer a monofont for formatting messages that were designed // to be displayed in a terminal. cell.textLabel.font = [UIFont monospacedSystemFontOfSize:14 weight:UIFontWeightBold]; - cell.textLabel.lineBreakMode = NSLineBreakByWordWrapping; - cell.textLabel.numberOfLines = 0; - cell.detailTextLabel.textColor = [UIColor whiteColor]; + cell.detailTextLabel.textColor = [RCTUIColor whiteColor]; // [macOS] +#if !TARGET_OS_OSX // [macOS] cell.backgroundColor = [UIColor colorWithRed:0.82 green:0.10 blue:0.15 alpha:1.0]; cell.selectionStyle = UITableViewCellSelectionStyleNone; - } - - cell.textLabel.text = message; - - return cell; -} #else // [macOS -- (NSTableCellView *)reuseCell:(NSTableCellView *)cell forErrorMessage:(NSString *)message -{ - if (!cell) { - cell = [[NSTableCellView alloc] initWithFrame:NSZeroRect]; - cell.rowSizeStyle = NSTableViewRowSizeStyleCustom; - cell.textField.accessibilityIdentifier = @"red box-error"; - - NSTextField *label = [[NSTextField alloc] initWithFrame:NSZeroRect]; - label.translatesAutoresizingMaskIntoConstraints = NO; - label.drawsBackground = NO; - label.bezeled = NO; - label.editable = NO; - - [cell addSubview:label]; - cell.textField = label; - - [NSLayoutConstraint activateConstraints:@[ - [[label leadingAnchor] constraintEqualToAnchor:[cell leadingAnchor] constant:5], - [[label topAnchor] constraintEqualToAnchor:[cell topAnchor] constant:5], - [[label trailingAnchor] constraintEqualToAnchor:[cell trailingAnchor] constant:-5], - [[label bottomAnchor] constraintEqualToAnchor:[cell bottomAnchor] constant:-5], - ]]; - - // Prefer a monofont for formatting messages that were designed - // to be displayed in a terminal. - cell.textField.font = [NSFont monospacedSystemFontOfSize:14 weight:NSFontWeightBold]; - - cell.textField.lineBreakMode = NSLineBreakByWordWrapping; - cell.textField.maximumNumberOfLines = 0; - cell.wantsLayer = true; + cell.wantsLayer = YES; + cell.layer.backgroundColor = [NSColor colorWithRed:0.82 green:0.10 blue:0.15 alpha:1.0].CGColor; cell.layer.cornerRadius = 8.0; cell.layer.cornerCurve = kCACornerCurveContinuous; - - cell.layer.backgroundColor = [NSColor colorWithRed:0.82 green:0.10 blue:0.15 alpha:1.0].CGColor; + cell.textLabel.selectable = YES; +#endif // macOS] } + // [macOS + cell.textLabel.accessibilityIdentifier = @"redbox-error"; + cell.textLabel.lineBreakMode = NSLineBreakByWordWrapping; + cell.textLabel.numberOfLines = 0; + // macOS] +#if !TARGET_OS_OSX // [macOS] + cell.textLabel.text = message; +#else // [macOS NSDictionary *attributes = @{ NSForegroundColorAttributeName : [NSColor whiteColor], NSFontAttributeName : [NSFont systemFontOfSize:16], }; NSAttributedString *title = [[NSAttributedString alloc] initWithString:message attributes:attributes]; - - cell.textField.attributedStringValue = title; + cell.textLabel.attributedStringValue = title; +#endif // macOS] return cell; } -#endif // [macOS] -#if !TARGET_OS_OSX // [macOS] -- (UITableViewCell *)reuseCell:(UITableViewCell *)cell forStackFrame:(RCTJSStackFrame *)stackFrame +- (RCTUITableViewCell *)reuseCell:(RCTUITableViewCell *)cell forStackFrame:(RCTJSStackFrame *)stackFrame // [macOS] { if (!cell) { - 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.textColor = [UIColor colorWithRed:0.70 green:0.70 blue:0.70 alpha:1.0]; + cell.detailTextLabel.textColor = [RCTUIColor colorWithRed:0.70 green:0.70 blue:0.70 alpha:1.0]; // [macOS] cell.detailTextLabel.font = [UIFont fontWithName:@"Menlo-Regular" size:11]; 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 = [UIColor colorWithWhite:0 alpha:0.2]; +#else // [macOS + [cell.detailTextLabel removeFromSuperview]; + [cell.textLabel.bottomAnchor constraintEqualToAnchor:cell.contentView.bottomAnchor constant:-5].active = YES; + cell.textLabel.selectable = YES; +#endif // macOS] } - cell.textLabel.text = stackFrame.methodName ?: @"(unnamed method)"; - if (stackFrame.file) { - cell.detailTextLabel.text = [self formatFrameSource:stackFrame]; - } else { - cell.detailTextLabel.text = @""; - } + // [macOS + NSString *text = stackFrame.methodName ?: @"(unnamed method)"; + cell.textLabel.lineBreakMode = NSLineBreakByCharWrapping; + cell.textLabel.numberOfLines = 2; + // macOS] + +#if !TARGET_OS_OSX // [macOS] + cell.textLabel.text = text; // [macOS] + cell.detailTextLabel.text = stackFrame.file ? [self formatFrameSource:stackFrame] : @""; // [macOS] cell.textLabel.textColor = stackFrame.collapse ? [UIColor lightGrayColor] : [UIColor whiteColor]; cell.detailTextLabel.textColor = stackFrame.collapse ? [UIColor colorWithRed:0.50 green:0.50 blue:0.50 alpha:1.0] : [UIColor colorWithRed:0.70 green:0.70 blue:0.70 alpha:1.0]; - return cell; -} #else // [macOS -- (NSTableCellView *)reuseCell:(NSTableCellView *)cell forStackFrame:(RCTJSStackFrame *)stackFrame -{ - if (!cell) { - cell = [[NSTableCellView alloc] initWithFrame:NSZeroRect]; - - NSTextField *label = [[NSTextField alloc] initWithFrame:NSZeroRect]; - label.translatesAutoresizingMaskIntoConstraints = NO; - label.backgroundColor = [NSColor clearColor]; - label.bezeled = NO; - label.editable = NO; - - label.maximumNumberOfLines = 2; - - [cell addSubview:label]; - cell.textField = label; - - [NSLayoutConstraint activateConstraints:@[ - [[label leadingAnchor] constraintEqualToAnchor:[cell leadingAnchor] constant:5], - [[label topAnchor] constraintEqualToAnchor:[cell topAnchor]], - [[label trailingAnchor] constraintEqualToAnchor:[cell trailingAnchor] constant:-5], - [[label bottomAnchor] constraintEqualToAnchor:[cell bottomAnchor]], - ]]; - } - - NSString *text = stackFrame.methodName ?: @"(unnamed method)"; - NSMutableParagraphStyle *textParagraphStyle = [NSMutableParagraphStyle new]; textParagraphStyle.lineBreakMode = NSLineBreakByCharWrapping; - + NSDictionary *textAttributes = @{ NSForegroundColorAttributeName : stackFrame.collapse ? [NSColor lightGrayColor] : [NSColor whiteColor], NSFontAttributeName : [NSFont fontWithName:@"Menlo-Regular" size:14], NSParagraphStyleAttributeName : textParagraphStyle, }; - + NSAttributedString *attributedText = [[NSAttributedString alloc] initWithString:text attributes:textAttributes]; - - NSMutableAttributedString *title = [attributedText mutableCopy]; // NSTableCellView doesn't contain a subtitle text field. Rather than define our own custom row view, // let's append the detail text with a new line if it is needed. + cell.textLabel.maximumNumberOfLines = stackFrame.file ? 3 : 2; if (stackFrame.file) { - cell.textField.maximumNumberOfLines = 3; - NSString *detailText = [self formatFrameSource:stackFrame]; - + NSMutableParagraphStyle *detailTextParagraphStyle = [NSMutableParagraphStyle new]; detailTextParagraphStyle.lineBreakMode = NSLineBreakByTruncatingMiddle; - + NSDictionary *detailTextAttributes = @{ NSForegroundColorAttributeName : stackFrame.collapse ? [NSColor colorWithRed:0.50 green:0.50 blue:0.50 alpha:1.0] : @@ -683,50 +585,42 @@ - (NSTableCellView *)reuseCell:(NSTableCellView *)cell forStackFrame:(RCTJSStack NSParagraphStyleAttributeName : detailTextParagraphStyle, }; NSAttributedString *attributedDetailText = [[NSAttributedString alloc] initWithString:detailText attributes:detailTextAttributes]; - + [title appendAttributedString:[[NSAttributedString alloc] initWithString:@"\n"]]; [title appendAttributedString:attributedDetailText]; } - - cell.textField.attributedStringValue = title; + cell.textLabel.attributedStringValue = title; +#endif // macOS] return cell; } -#endif // macOS] -#if !TARGET_OS_OSX // [macOS] -- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath -#else // [macOS -- (CGFloat)tableView:(NSTableView *)tableView heightOfRow:(NSInteger)row -#endif // macOS] +- (CGFloat)tableView:(RCTUITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath // [macOS] { -#if !TARGET_OS_OSX // [macOS] if (indexPath.section == 0) { -#else // [macOS - if (row == 0) { -#endif // macOS] NSMutableParagraphStyle *paragraphStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy]; paragraphStyle.lineBreakMode = NSLineBreakByWordWrapping; - NSDictionary *attributes = -#if !TARGET_OS_OSX // [macOS] - @{NSFontAttributeName : [UIFont boldSystemFontOfSize:16], NSParagraphStyleAttributeName : paragraphStyle}; -#else // [macOS - @{NSFontAttributeName : [NSFont boldSystemFontOfSize:16], NSParagraphStyleAttributeName : paragraphStyle}; + NSDictionary *attributes = @{ + NSFontAttributeName : [UIFont boldSystemFontOfSize:16], + NSParagraphStyleAttributeName : paragraphStyle + }; // [macOS] + CGFloat tableWidth = tableView.frame.size.width; // [macOS] +#if TARGET_OS_OSX // [macOS + tableWidth = tableView.contentSize.width; #endif // macOS] CGRect boundingRect = - [_lastErrorMessage boundingRectWithSize:CGSizeMake(tableView.frame.size.width - 30, CGFLOAT_MAX) + [_lastErrorMessage boundingRectWithSize:CGSizeMake(tableWidth - 30, CGFLOAT_MAX) options:NSStringDrawingUsesLineFragmentOrigin attributes:attributes - context:nil]; + context:nil]; // [macOS] return ceil(boundingRect.size.height) + 40; } else { return 50; } } -#if !TARGET_OS_OSX // [macOS -- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath +- (void)tableView:(RCTUITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath // [macOS] { if (indexPath.section == 1) { NSUInteger row = indexPath.row; @@ -735,17 +629,6 @@ - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath } [tableView deselectRowAtIndexPath:indexPath animated:YES]; } -#else // [macOS -- (BOOL)tableView:(__unused NSTableView *)tableView shouldSelectRow:(__unused NSInteger)row -{ - if (row != 0) { - NSUInteger index = row - 1; - RCTJSStackFrame *stackFrame = _lastStackTrace[index]; - [_actionDelegate redBoxController:self openStackFrameInEditor:stackFrame]; - } - return NO; -} -#endif // macOS] #pragma mark - Key commands diff --git a/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUIKit.h b/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUIKit.h index eccebc648400..e1a2f0ae0228 100644 --- a/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUIKit.h +++ b/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUIKit.h @@ -16,6 +16,7 @@ #import "RCTUIView.h" #import "RCTUIScrollView.h" #import "RCTUISlider.h" +#import "RCTUITableView.h" #import "RCTUILabel.h" #import "RCTUISwitch.h" #import "RCTUIActivityIndicatorView.h" diff --git a/packages/react-native/ReactApple/Libraries/RCTUIKit/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..7332c4b9966e --- /dev/null +++ b/packages/react-native/ReactApple/Libraries/RCTUIKit/RCTUITableView.h @@ -0,0 +1,107 @@ +/* + * 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 + +@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 + +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) RCTPlatformColor *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) RCTPlatformColor *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..10ec00187e0f --- /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:(RCTPlatformColor *)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:(RCTPlatformColor *)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 // TARGET_OS_OSX