@onekeyfe/react-native-pager-view 6.7.0-alpha.436 → 6.7.0-alpha.449

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. package/README.md +16 -0
  2. package/android/build.gradle +1 -0
  3. package/android/src/androidTest/java/com/reactnativepagerview/CollapsiblePagerTabReservationTest.kt +135 -0
  4. package/android/src/main/java/com/reactnativepagerview/CollapsiblePagerHost.kt +88 -12
  5. package/android/src/main/java/com/reactnativepagerview/CollapsiblePagerNativeHeaders.kt +442 -15
  6. package/android/src/main/java/com/reactnativepagerview/CollapsiblePagerViewManager.kt +11 -1
  7. package/android/src/main/java/com/reactnativepagerview/event/NativeHeaderPressEvent.kt +8 -0
  8. package/docs/SPEC.md +298 -3
  9. package/ios/RNCCollapsiblePagerViewComponentView.mm +834 -20
  10. package/ios/tests/RNCCollapsiblePagerFirstFrameTests.mm +61 -0
  11. package/ios/tests/RNCCollapsiblePagerHitTestTests.mm +22 -0
  12. package/lib/module/CollapsiblePagerView.js +37 -5
  13. package/lib/module/CollapsiblePagerView.web.js +519 -6
  14. package/lib/module/CollapsiblePagerViewNativeComponent.ts +6 -0
  15. package/lib/module/NativeScrollerNativeComponent.ts +0 -1
  16. package/lib/typescript/src/CollapsiblePagerView.d.ts +31 -2
  17. package/lib/typescript/src/CollapsiblePagerView.web.d.ts +44 -1
  18. package/lib/typescript/src/CollapsiblePagerViewNativeComponent.d.ts +11 -0
  19. package/lib/typescript/src/NativeScroller.d.ts +2 -2
  20. package/package.json +3 -2
  21. package/src/CollapsiblePagerView.tsx +68 -5
  22. package/src/CollapsiblePagerView.web.tsx +464 -5
  23. package/src/CollapsiblePagerViewNativeComponent.ts +6 -0
  24. package/src/NativeScroller.tsx +2 -3
  25. package/src/NativeScrollerNativeComponent.ts +0 -1
@@ -14,6 +14,7 @@
14
14
  #import "React/RCTSurfaceTouchHandler.h"
15
15
  #import "React/RCTTouchHandler.h"
16
16
  #import <objc/runtime.h>
17
+ #import <React/RCTConvert.h>
17
18
  #import <UIKit/UIGestureRecognizerSubclass.h>
18
19
 
19
20
  // OneKey patch: deduplicate structural mutation parents before scanning the page.
@@ -398,8 +399,65 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
398
399
  (long)pagerState]);
399
400
  }
400
401
 
402
+ static CGFloat RNCPagerToolbarDimension(NSDictionary *entry, NSString *key, CGFloat fallback, CGFloat minimum, CGFloat maximum)
403
+ {
404
+ id value = entry[key];
405
+ if (![value isKindOfClass:NSNumber.class]) return fallback;
406
+ double dimension = [value doubleValue];
407
+ return std::isfinite(dimension) ? MIN(maximum, MAX(minimum, dimension)) : fallback;
408
+ }
409
+
410
+ @protocol RNCPagerNamedSVGBridge <NSObject>
411
+ + (void)cancelWithOwner:(id)owner;
412
+ + (void)requestWithOwner:(id)owner name:(NSString *)name color:(NSString *)color
413
+ width:(NSInteger)width height:(NSInteger)height scale:(double)scale
414
+ complete:(void (^)(UIImage *))complete;
415
+ @end
416
+
417
+ @interface RNCPagerToolbarButton : UIButton
418
+ @property (nonatomic, strong) UIColor *baseColor;
419
+ @property (nonatomic, strong) UIColor *hoverColor;
420
+ @property (nonatomic, strong) UIColor *pressedColor;
421
+ @property (nonatomic) BOOL hovering;
422
+ @property (nonatomic) CGFloat iconSize;
423
+ - (void)refreshBackground;
424
+ @end
425
+ @implementation RNCPagerToolbarButton
426
+ - (instancetype)initWithFrame:(CGRect)frame {
427
+ if ((self = [super initWithFrame:frame])) {
428
+ self.adjustsImageWhenHighlighted = NO;
429
+ [self addGestureRecognizer:[[UIHoverGestureRecognizer alloc] initWithTarget:self action:@selector(hover:)]];
430
+ }
431
+ return self;
432
+ }
433
+ - (CGRect)imageRectForContentRect:(CGRect)contentRect {
434
+ CGFloat side = MIN(self.iconSize, MIN(CGRectGetWidth(contentRect), CGRectGetHeight(contentRect)));
435
+ return CGRectMake(CGRectGetMidX(contentRect) - side / 2, CGRectGetMidY(contentRect) - side / 2, side, side);
436
+ }
437
+ - (void)hover:(UIHoverGestureRecognizer *)gesture {
438
+ self.hovering = gesture.state == UIGestureRecognizerStateBegan || gesture.state == UIGestureRecognizerStateChanged;
439
+ [self refreshBackground];
440
+ }
441
+ - (void)setEnabled:(BOOL)enabled {
442
+ [super setEnabled:enabled];
443
+ if (!enabled) self.hovering = NO;
444
+ [self refreshBackground];
445
+ }
446
+ - (void)setHighlighted:(BOOL)highlighted {
447
+ [super setHighlighted:highlighted];
448
+ [self refreshBackground];
449
+ }
450
+ - (void)refreshBackground {
451
+ self.backgroundColor = self.enabled && self.highlighted ? self.pressedColor : (self.enabled && self.hovering ? self.hoverColor : self.baseColor);
452
+ }
453
+ @end
454
+
401
455
  @interface RNCCollapsiblePagerNativeTabBarView : UIView <UIScrollViewDelegate>
402
456
  @property (nonatomic, copy) RNCCollapsiblePagerNativeTabPressHandler onTabPress;
457
+ @property (nonatomic, copy) void (^onToolbarPress)(NSString *, CGRect);
458
+ @property (nonatomic, copy) void (^onToolbarNeedsSettlement)(void);
459
+ - (void)updateToolbarPagerIndex:(NSInteger)index busy:(BOOL)busy;
460
+ - (void)updateConfigJSON:(NSString *)json;
403
461
  - (void)updateItemsJSON:(NSString *)itemsJSON;
404
462
  - (void)updateStyleWithHeight:(CGFloat)height
405
463
  variant:(NSString *)variant
@@ -415,6 +473,8 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
415
473
  indicatorHeight:(CGFloat)indicatorHeight
416
474
  indicatorBottom:(CGFloat)indicatorBottom;
417
475
  - (void)setProgress:(CGFloat)progress;
476
+ - (BOOL)usesControlledTextProgress;
477
+ - (BOOL)ownsSettledToolbar;
418
478
  - (void)setLeftToRight:(BOOL)leftToRight;
419
479
  - (void)updateAncestorGesturePrecedence;
420
480
  @end
@@ -430,6 +490,40 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
430
490
  CGFloat _contentPaddingHorizontal;
431
491
  CGFloat _contentPaddingVertical;
432
492
  BOOL _segmented;
493
+ BOOL _textVariant;
494
+ NSDictionary *_config;
495
+ RNCPagerToolbarButton *_toolbarButton;
496
+ NSString *_toolbarKey;
497
+ BOOL _toolbarPagerBusy;
498
+ NSInteger _toolbarPageIndex;
499
+ NSDictionary *_committedToolbarEntry;
500
+ NSString *_desiredToolbarKey;
501
+ NSDictionary *_desiredToolbarEntry;
502
+ UIImage *_loadedToolbarImage;
503
+ BOOL _toolbarPresenceAnimating;
504
+ BOOL _toolbarPresenceInterrupted;
505
+ NSString *_toolbarTouchKey;
506
+ BOOL _toolbarTouchStarted;
507
+ NSString *_iconIdentity;
508
+ NSObject *_iconOwner;
509
+ NSInteger _progressSource;
510
+ NSInteger _progressDestination;
511
+ CGFloat _progressFraction;
512
+ CGFloat _nativeProgress;
513
+ BOOL _hasNativeProgress;
514
+ BOOL _hasMappedPageProgress;
515
+ CGFloat _toolbarReservedWidth;
516
+ BOOL _toolbarVisible;
517
+ NSUInteger _toolbarAnimationGeneration;
518
+ NSString *_loadedIconIdentity;
519
+ BOOL _iconRequestPending;
520
+ UIImage *_readyToolbarImage;
521
+ NSString *_readyToolbarIdentity;
522
+ NSString *_failedIconIdentity;
523
+ CGFloat _toolbarSize;
524
+ CGFloat _toolbarIconSize;
525
+ CGFloat _toolbarContentWidth;
526
+ CGFloat _toolbarTrailingInset;
433
527
  CGFloat _itemSpacing;
434
528
  CGFloat _fontSize;
435
529
  NSString *_fontFamily;
@@ -448,6 +542,21 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
448
542
  {
449
543
  if (self = [super initWithFrame:frame]) {
450
544
  _items = @[];
545
+ _config = @{};
546
+ _toolbarPagerBusy = YES;
547
+ _toolbarPageIndex = -1;
548
+ _toolbarButton = [RNCPagerToolbarButton new];
549
+ _toolbarButton.layer.cornerRadius = 19;
550
+ _toolbarButton.hidden = YES;
551
+ _toolbarButton.alpha = 0;
552
+ _toolbarButton.userInteractionEnabled = NO;
553
+ _toolbarButton.accessibilityElementsHidden = YES;
554
+ _toolbarButton.imageView.contentMode = UIViewContentModeScaleAspectFit;
555
+ _toolbarButton.iconSize = 24;
556
+ [_toolbarButton addTarget:self action:@selector(toolbarTouchDown) forControlEvents:UIControlEventTouchDown];
557
+ [_toolbarButton addTarget:self action:@selector(toolbarPress:forEvent:) forControlEvents:UIControlEventTouchUpInside];
558
+ [_toolbarButton addTarget:self action:@selector(toolbarCancel) forControlEvents:UIControlEventTouchCancel | UIControlEventTouchUpOutside];
559
+ [self addSubview:_toolbarButton];
451
560
  _buttons = [NSMutableArray new];
452
561
  _indicatorFrames = [NSMutableArray new];
453
562
  _barHeight = 44;
@@ -529,6 +638,11 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
529
638
  for (UIButton *button in _buttons) [_scrollView bringSubviewToFront:button];
530
639
  _shouldCenterSelectedItem = YES;
531
640
  self.hidden = _items.count == 0;
641
+ if (_hasNativeProgress) [self setProgress:_nativeProgress];
642
+ else if ([_config[@"controlled"] boolValue]) {
643
+ NSUInteger selected = [_items indexOfObjectPassingTest:^BOOL(NSDictionary *item, NSUInteger i, BOOL *stop) { return [item[@"key"] isEqual:self->_config[@"selectedKey"]]; }];
644
+ if (selected != NSNotFound) { _progress = selected; _progressSource = selected; _progressDestination = selected; _progressFraction = 0; }
645
+ }
532
646
  [self setNeedsLayout];
533
647
  }
534
648
 
@@ -549,6 +663,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
549
663
  _barHeight = MAX(1, height);
550
664
  BOOL wasSegmented = _segmented;
551
665
  _segmented = [variant isEqualToString:@"segmented"];
666
+ _textVariant = [variant isEqualToString:@"text"];
552
667
  _contentPaddingHorizontal = MAX(0, contentPaddingHorizontal);
553
668
  _contentPaddingVertical = MAX(0, contentPaddingVertical);
554
669
  _itemSpacing = MAX(0, itemSpacing);
@@ -624,9 +739,15 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
624
739
  - (void)layoutSubviews
625
740
  {
626
741
  [super layoutSubviews];
627
- _scrollView.frame = self.bounds;
742
+ CGFloat reserved = _toolbarReservedWidth;
743
+ [self bringSubviewToFront:_toolbarButton];
744
+ _scrollView.frame = CGRectMake(_leftToRight ? 0 : reserved, 0,
745
+ MAX(0, CGRectGetWidth(self.bounds) - reserved), CGRectGetHeight(self.bounds));
746
+ CGFloat inset = _toolbarTrailingInset - (_toolbarSize - _toolbarContentWidth) / 2;
747
+ _toolbarButton.frame = CGRectMake(_leftToRight ? CGRectGetWidth(self.bounds) - inset - _toolbarSize : inset,
748
+ (CGRectGetHeight(self.bounds) - _toolbarSize) / 2, _toolbarSize, _toolbarSize);
628
749
  if (_segmented) {
629
- CGFloat width = CGRectGetWidth(self.bounds);
750
+ CGFloat width = CGRectGetWidth(_scrollView.bounds);
630
751
  CGFloat height = CGRectGetHeight(self.bounds);
631
752
  CGFloat insetX = MIN(_contentPaddingHorizontal, width / 2);
632
753
  CGFloat insetY = MIN(_contentPaddingVertical, height / 2);
@@ -634,7 +755,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
634
755
  MAX(0, width - 2 * insetX), MAX(0, height - 2 * insetY));
635
756
  _trackView.frame = trackFrame;
636
757
  _trackView.layer.cornerRadius = CGRectGetHeight(trackFrame) / 2;
637
- _scrollView.contentSize = self.bounds.size;
758
+ _scrollView.contentSize = _scrollView.bounds.size;
638
759
  [_indicatorFrames removeAllObjects];
639
760
  NSUInteger count = _buttons.count;
640
761
  [_buttons enumerateObjectsUsingBlock:^(UIButton *button, NSUInteger index, BOOL *stop) {
@@ -658,12 +779,17 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
658
779
  for (UIButton *button in _buttons) {
659
780
  NSString *title = [button titleForState:UIControlStateNormal] ?: @"";
660
781
  CGFloat textWidth = ceil([title sizeWithAttributes:@{NSFontAttributeName: font}].width);
661
- CGFloat buttonWidth = MAX(44, textWidth + 16);
782
+ if (_textVariant) {
783
+ UIFont *activeFont = [UIFont fontWithName:_config[@"activeFontFamily"] ?: @"" size:_fontSize]
784
+ ?: [UIFont systemFontOfSize:_fontSize weight:UIFontWeightSemibold];
785
+ textWidth = MAX(textWidth, ceil([title sizeWithAttributes:@{NSFontAttributeName: activeFont}].width));
786
+ }
787
+ CGFloat buttonWidth = MAX(44, textWidth + (_textVariant ? 20 : 16));
662
788
  [widths addObject:@(buttonWidth)];
663
789
  itemsWidth += buttonWidth;
664
790
  }
665
791
  if (_buttons.count > 1) itemsWidth += (_buttons.count - 1) * _itemSpacing;
666
- CGFloat contentWidth = MAX(CGRectGetWidth(self.bounds),
792
+ CGFloat contentWidth = MAX(CGRectGetWidth(_scrollView.bounds),
667
793
  itemsWidth + _contentPaddingHorizontal * 2);
668
794
  _scrollView.contentSize = CGSizeMake(contentWidth, MAX(_barHeight, CGRectGetHeight(self.bounds)));
669
795
  [_indicatorFrames removeAllObjects];
@@ -693,14 +819,493 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
693
819
  [self updatePresentation];
694
820
  }
695
821
 
822
+ - (Class<RNCPagerNamedSVGBridge>)iconBridge {
823
+ Class cls = NSClassFromString(@"OneKeyNamedSVGBridge");
824
+ if (![cls respondsToSelector:@selector(cancelWithOwner:)] ||
825
+ ![cls respondsToSelector:@selector(requestWithOwner:name:color:width:height:scale:complete:)]) return Nil;
826
+ return (Class<RNCPagerNamedSVGBridge>)cls;
827
+ }
828
+ - (void)updateConfigJSON:(NSString *)json {
829
+ BOOL wasOwned = [self ownsSettledToolbar];
830
+ NSData *data = [json dataUsingEncoding:NSUTF8StringEncoding];
831
+ id parsed = data ? [NSJSONSerialization JSONObjectWithData:data options:0 error:nil] : nil;
832
+ _config = [parsed isKindOfClass:NSDictionary.class] ? parsed : @{};
833
+ BOOL owned = [self ownsSettledToolbar];
834
+ if (owned != wasOwned) {
835
+ if (_iconOwner) [[self iconBridge] cancelWithOwner:_iconOwner];
836
+ _iconOwner = [NSObject new];
837
+ _iconRequestPending = NO;
838
+ _readyToolbarImage = nil;
839
+ _readyToolbarIdentity = nil;
840
+ _desiredToolbarKey = nil;
841
+ _desiredToolbarEntry = nil;
842
+ [self suspendToolbar];
843
+ if (!owned) {
844
+ ++_toolbarAnimationGeneration;
845
+ [_toolbarButton.layer removeAllAnimations];
846
+ _toolbarPresenceAnimating = NO;
847
+ _toolbarPresenceInterrupted = NO;
848
+ _toolbarButton.alpha = _toolbarVisible ? 1 : 0;
849
+ _toolbarButton.hidden = !_toolbarVisible;
850
+ [self setToolbarInteractive:_toolbarVisible];
851
+ }
852
+ }
853
+ if (!owned) {
854
+ _toolbarTouchKey = nil;
855
+ _toolbarButton.highlighted = NO;
856
+ _toolbarButton.hovering = NO;
857
+ }
858
+ _toolbarButton.adjustsImageWhenDisabled = !owned;
859
+ NSString *key = _config[@"selectedKey"];
860
+ if (![_config[@"controlled"] boolValue]) {
861
+ _progress = RNCClamp(_nativeProgress, 0, MAX(0, (NSInteger)_items.count - 1));
862
+ key = _items.count ? _items[(NSInteger)round(_progress)][@"key"] : nil;
863
+ }
864
+ if (_hasNativeProgress) {
865
+ [self setProgress:_nativeProgress];
866
+ } else {
867
+ NSUInteger index = [_items indexOfObjectPassingTest:^BOOL(NSDictionary *item, NSUInteger i, BOOL *stop) { return [item[@"key"] isEqual:key]; }];
868
+ if (index != NSNotFound) {
869
+ _progress = index;
870
+ _progressSource = index;
871
+ _progressDestination = index;
872
+ _progressFraction = 0;
873
+ }
874
+ }
875
+ [self updateToolbarForKey:key];
876
+ [self setNeedsLayout];
877
+ }
878
+ - (BOOL)usesControlledTextProgress { return _textVariant && [_config[@"controlled"] boolValue]; }
879
+ - (void)setLegacyToolbarVisible:(BOOL)visible {
880
+ _toolbarButton.enabled = visible;
881
+ _toolbarButton.userInteractionEnabled = visible;
882
+ _toolbarButton.accessibilityElementsHidden = !visible;
883
+ _toolbarButton.isAccessibilityElement = visible;
884
+ if (_toolbarVisible == visible) return;
885
+ _toolbarVisible = visible;
886
+ NSUInteger generation = ++_toolbarAnimationGeneration;
887
+ CALayer *presentation = _toolbarButton.layer.presentationLayer;
888
+ CGFloat alpha = presentation ? presentation.opacity : _toolbarButton.alpha;
889
+ [_toolbarButton.layer removeAllAnimations];
890
+ _toolbarButton.alpha = alpha;
891
+ _toolbarButton.enabled = visible;
892
+ _toolbarButton.userInteractionEnabled = visible;
893
+ _toolbarButton.accessibilityElementsHidden = !visible;
894
+ if (visible) _toolbarButton.hidden = NO;
895
+ if (self.window == nil) {
896
+ _toolbarButton.alpha = visible ? 1 : 0;
897
+ _toolbarButton.hidden = !visible;
898
+ return;
899
+ }
900
+ [UIView animateWithDuration:0.15 delay:0 options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionAllowUserInteraction animations:^{
901
+ self->_toolbarButton.alpha = visible ? 1 : 0;
902
+ } completion:^(BOOL finished) {
903
+ if (self->_toolbarAnimationGeneration == generation && !visible) {
904
+ self->_toolbarButton.hidden = YES;
905
+ [self applyReadyLegacyToolbarImage];
906
+ }
907
+ }];
908
+ }
909
+ - (void)applyReadyLegacyToolbarImage {
910
+ if (_readyToolbarImage == nil || ![_readyToolbarIdentity isEqualToString:_iconIdentity]) return;
911
+ CALayer *presentation = _toolbarButton.layer.presentationLayer;
912
+ if (_toolbarButton.alpha > 0.001 || (presentation && presentation.opacity > 0.001)) return;
913
+ [_toolbarButton setImage:_readyToolbarImage forState:UIControlStateNormal];
914
+ _loadedIconIdentity = _readyToolbarIdentity;
915
+ _loadedToolbarImage = _readyToolbarImage;
916
+ _readyToolbarImage = nil;
917
+ _readyToolbarIdentity = nil;
918
+ _iconRequestPending = NO;
919
+ [self setLegacyToolbarVisible:YES];
920
+ }
921
+ - (void)updateLegacyToolbarForKey:(NSString *)key {
922
+ NSDictionary *toolbar = [_config[@"toolbar"] isKindOfClass:NSDictionary.class] ? _config[@"toolbar"] : nil;
923
+ _toolbarReservedWidth = toolbar == nil ? 0 : 44;
924
+ if (toolbar.count > 0) {
925
+ _toolbarReservedWidth = 0;
926
+ for (id value in toolbar.allValues) {
927
+ NSDictionary *entry = [value isKindOfClass:NSDictionary.class] ? value : @{};
928
+ CGFloat size = RNCPagerToolbarDimension(entry, @"size", 38, 1, 128);
929
+ CGFloat content = RNCPagerToolbarDimension(entry, @"contentWidth", MIN(24, size), 0, size);
930
+ CGFloat trailing = RNCPagerToolbarDimension(entry, @"trailingInset", 20, 0, 128);
931
+ _toolbarReservedWidth = MAX(_toolbarReservedWidth, content + trailing);
932
+ }
933
+ }
934
+ NSDictionary *entry = [toolbar[key ?: @""] isKindOfClass:NSDictionary.class] ? toolbar[key] : nil;
935
+ NSString *name = [entry[@"iconName"] isKindOfClass:NSString.class] ? entry[@"iconName"] : nil;
936
+ if (![_toolbarKey isEqualToString:key]) {
937
+ _toolbarTouchKey = nil;
938
+ _toolbarButton.highlighted = NO;
939
+ _toolbarButton.hovering = NO;
940
+ }
941
+ _toolbarKey = [key copy];
942
+ if (name.length == 0) {
943
+ _toolbarTouchKey = nil;
944
+ _toolbarButton.accessibilityLabel = nil;
945
+ _toolbarButton.accessibilityIdentifier = nil;
946
+ if (_iconOwner) [[self iconBridge] cancelWithOwner:_iconOwner];
947
+ _iconOwner = [NSObject new];
948
+ _iconRequestPending = NO;
949
+ _readyToolbarImage = nil;
950
+ _readyToolbarIdentity = nil;
951
+ [self setLegacyToolbarVisible:NO];
952
+ return;
953
+ }
954
+ _toolbarSize = RNCPagerToolbarDimension(entry, @"size", 38, 1, 128);
955
+ _toolbarIconSize = RNCPagerToolbarDimension(entry, @"iconSize", MIN(24, _toolbarSize), 1, _toolbarSize);
956
+ _toolbarContentWidth = RNCPagerToolbarDimension(entry, @"contentWidth", MIN(24, _toolbarSize), 0, _toolbarSize);
957
+ _toolbarTrailingInset = RNCPagerToolbarDimension(entry, @"trailingInset", 20, 0, 128);
958
+ _toolbarButton.layer.cornerRadius = _toolbarSize / 2;
959
+ _toolbarButton.iconSize = _toolbarIconSize;
960
+ [_toolbarButton setNeedsLayout];
961
+ _toolbarButton.baseColor = [entry[@"selected"] boolValue] ? ([RCTConvert UIColor:entry[@"selectedBackgroundColor"]] ?: UIColor.clearColor) : UIColor.clearColor;
962
+ _toolbarButton.hoverColor = [RCTConvert UIColor:entry[@"hoverBackgroundColor"]] ?: _toolbarButton.baseColor;
963
+ _toolbarButton.pressedColor = [RCTConvert UIColor:entry[@"pressedBackgroundColor"]] ?: _toolbarButton.baseColor;
964
+ [_toolbarButton refreshBackground];
965
+ _toolbarButton.accessibilityLabel = entry[@"accessibilityLabel"];
966
+ _toolbarButton.accessibilityIdentifier = entry[@"testID"];
967
+ NSString *color = [entry[@"iconColor"] isKindOfClass:NSString.class] ? entry[@"iconColor"] : @"#000000";
968
+ CGFloat scale = self.window.screen.scale ?: UIScreen.mainScreen.scale;
969
+ NSString *identity = [NSString stringWithFormat:@"%@|%@|%g|%g", name, color, scale, _toolbarIconSize];
970
+ if ([_loadedIconIdentity isEqualToString:identity] && [_toolbarButton imageForState:UIControlStateNormal] != nil) {
971
+ if (_iconRequestPending) {
972
+ [[self iconBridge] cancelWithOwner:_iconOwner];
973
+ _iconOwner = [NSObject new];
974
+ }
975
+ _iconIdentity = identity;
976
+ _iconRequestPending = NO;
977
+ _readyToolbarImage = nil;
978
+ _readyToolbarIdentity = nil;
979
+ [self setLegacyToolbarVisible:YES];
980
+ return;
981
+ }
982
+ if ((_iconRequestPending || [_failedIconIdentity isEqualToString:identity]) && [_iconIdentity isEqualToString:identity]) return;
983
+ if (_iconOwner) [[self iconBridge] cancelWithOwner:_iconOwner];
984
+ NSObject *owner = [NSObject new];
985
+ _iconOwner = owner;
986
+ _iconIdentity = identity;
987
+ _iconRequestPending = NO;
988
+ _readyToolbarImage = nil;
989
+ _readyToolbarIdentity = nil;
990
+ [self setLegacyToolbarVisible:NO];
991
+ if ([_failedIconIdentity isEqualToString:identity] || self.window == nil) return;
992
+ _iconRequestPending = YES;
993
+ __weak __typeof__(self) weakSelf = self;
994
+ [[self iconBridge] requestWithOwner:owner name:name color:color width:(NSInteger)ceil(_toolbarIconSize * scale) height:(NSInteger)ceil(_toolbarIconSize * scale) scale:scale complete:^(UIImage *image) {
995
+ __strong __typeof__(weakSelf) self = weakSelf;
996
+ if (!self || self->_iconOwner != owner || ![self->_iconIdentity isEqualToString:identity] || !self.window) return;
997
+ if (image == nil) {
998
+ self->_iconRequestPending = NO;
999
+ self->_failedIconIdentity = identity;
1000
+ return;
1001
+ }
1002
+ UIImage *scaled = image.CGImage ? [UIImage imageWithCGImage:image.CGImage scale:scale orientation:UIImageOrientationUp] : image;
1003
+ self->_readyToolbarImage = [scaled imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal];
1004
+ self->_readyToolbarIdentity = identity;
1005
+ [self applyReadyLegacyToolbarImage];
1006
+ }];
1007
+ }
1008
+
1009
+ - (BOOL)ownsSettledToolbar {
1010
+ return [_config[@"controlled"] boolValue] && [_config[@"toolbar"] isKindOfClass:NSDictionary.class];
1011
+ }
1012
+ - (BOOL)toolbarCanCommit {
1013
+ if (!self.window) return NO;
1014
+ if (![self ownsSettledToolbar]) return YES;
1015
+ if (_toolbarPagerBusy || _toolbarPageIndex < 0) return NO;
1016
+ NSArray *pages = [_config[@"pageKeys"] isKindOfClass:NSArray.class] ? _config[@"pageKeys"] : @[];
1017
+ NSArray *items = [_items valueForKey:@"key"];
1018
+ BOOL valid = pages.count > 0 && [pages containsObject:_config[@"selectedKey"] ?: @""] &&
1019
+ [NSSet setWithArray:pages].count == pages.count && [NSSet setWithArray:items].count == items.count;
1020
+ for (id key in pages) if (![items containsObject:key]) { valid = NO; break; }
1021
+ return !valid || (_toolbarPageIndex < pages.count && [pages[_toolbarPageIndex] isEqual:_config[@"selectedKey"]]);
1022
+ }
1023
+ - (void)setToolbarInteractive:(BOOL)interactive {
1024
+ _toolbarButton.enabled = interactive;
1025
+ _toolbarButton.userInteractionEnabled = interactive;
1026
+ _toolbarButton.accessibilityElementsHidden = !interactive;
1027
+ _toolbarButton.isAccessibilityElement = interactive;
1028
+ if (!interactive) {
1029
+ _toolbarTouchKey = nil;
1030
+ _toolbarButton.highlighted = NO;
1031
+ _toolbarButton.hovering = NO;
1032
+ [_toolbarButton refreshBackground];
1033
+ }
1034
+ }
1035
+ - (void)cancelToolbarRequest {
1036
+ if (_iconRequestPending || _readyToolbarImage) {
1037
+ if (_iconOwner) [[self iconBridge] cancelWithOwner:_iconOwner];
1038
+ _iconOwner = [NSObject new];
1039
+ _iconRequestPending = NO;
1040
+ _readyToolbarImage = nil;
1041
+ _readyToolbarIdentity = nil;
1042
+ }
1043
+ }
1044
+ - (void)suspendToolbar {
1045
+ [self setToolbarInteractive:NO];
1046
+ [self cancelToolbarRequest];
1047
+ _desiredToolbarKey = nil;
1048
+ _desiredToolbarEntry = nil;
1049
+ if (_toolbarPresenceAnimating) {
1050
+ CALayer *presentation = _toolbarButton.layer.presentationLayer;
1051
+ CGFloat alpha = presentation ? presentation.opacity : _toolbarButton.alpha;
1052
+ ++_toolbarAnimationGeneration;
1053
+ [_toolbarButton.layer removeAllAnimations];
1054
+ _toolbarButton.alpha = alpha;
1055
+ _toolbarPresenceAnimating = NO;
1056
+ _toolbarPresenceInterrupted = YES;
1057
+ }
1058
+ }
1059
+ - (void)updateToolbarPagerIndex:(NSInteger)index busy:(BOOL)busy {
1060
+ if (_toolbarPagerBusy == busy && (busy || _toolbarPageIndex == index)) return;
1061
+ _toolbarPagerBusy = busy;
1062
+ if (!busy) _toolbarPageIndex = index;
1063
+ NSString *key = [_config[@"controlled"] boolValue] ? _config[@"selectedKey"] : _toolbarKey;
1064
+ [self updateToolbarForKey:key];
1065
+ }
1066
+ - (void)setToolbarVisible:(BOOL)visible {
1067
+ if (_toolbarVisible == visible && !_toolbarPresenceInterrupted) return;
1068
+ _toolbarVisible = visible;
1069
+ _toolbarPresenceInterrupted = NO;
1070
+ NSUInteger generation = ++_toolbarAnimationGeneration;
1071
+ CALayer *presentation = _toolbarButton.layer.presentationLayer;
1072
+ CGFloat alpha = presentation ? presentation.opacity : _toolbarButton.alpha;
1073
+ [_toolbarButton.layer removeAllAnimations];
1074
+ _toolbarButton.alpha = alpha;
1075
+ if (visible) _toolbarButton.hidden = NO;
1076
+ if (!self.window) {
1077
+ _toolbarPresenceAnimating = NO;
1078
+ _toolbarButton.alpha = visible ? 1 : 0;
1079
+ _toolbarButton.hidden = !visible;
1080
+ return;
1081
+ }
1082
+ _toolbarPresenceAnimating = YES;
1083
+ [UIView animateWithDuration:0.10 delay:0 options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionAllowUserInteraction animations:^{
1084
+ self->_toolbarButton.alpha = visible ? 1 : 0;
1085
+ } completion:^(BOOL finished) {
1086
+ if (self->_toolbarAnimationGeneration != generation) return;
1087
+ self->_toolbarPresenceAnimating = NO;
1088
+ if (!visible) self->_toolbarButton.hidden = YES;
1089
+ }];
1090
+ }
1091
+ - (void)commitToolbarImage:(UIImage *)image identity:(NSString *)identity {
1092
+ if (![self toolbarCanCommit] || !_desiredToolbarKey || !_desiredToolbarEntry) return;
1093
+ NSDictionary *entry = _desiredToolbarEntry;
1094
+ BOOL changed = ![_toolbarKey isEqualToString:_desiredToolbarKey] || ![_committedToolbarEntry isEqual:entry] || _loadedToolbarImage != image;
1095
+ if (![_toolbarKey isEqualToString:_desiredToolbarKey]) [self setToolbarInteractive:NO];
1096
+ _toolbarKey = [_desiredToolbarKey copy];
1097
+ _committedToolbarEntry = entry;
1098
+ _toolbarSize = RNCPagerToolbarDimension(entry, @"size", 38, 1, 128);
1099
+ _toolbarIconSize = RNCPagerToolbarDimension(entry, @"iconSize", MIN(24, _toolbarSize), 1, _toolbarSize);
1100
+ _toolbarContentWidth = RNCPagerToolbarDimension(entry, @"contentWidth", MIN(24, _toolbarSize), 0, _toolbarSize);
1101
+ _toolbarTrailingInset = RNCPagerToolbarDimension(entry, @"trailingInset", 20, 0, 128);
1102
+ _toolbarButton.layer.cornerRadius = _toolbarSize / 2;
1103
+ _toolbarButton.iconSize = _toolbarIconSize;
1104
+ [_toolbarButton setNeedsLayout];
1105
+ _toolbarButton.baseColor = [entry[@"selected"] boolValue] ? ([RCTConvert UIColor:entry[@"selectedBackgroundColor"]] ?: UIColor.clearColor) : UIColor.clearColor;
1106
+ _toolbarButton.hoverColor = [RCTConvert UIColor:entry[@"hoverBackgroundColor"]] ?: _toolbarButton.baseColor;
1107
+ _toolbarButton.pressedColor = [RCTConvert UIColor:entry[@"pressedBackgroundColor"]] ?: _toolbarButton.baseColor;
1108
+ [_toolbarButton refreshBackground];
1109
+ _toolbarButton.accessibilityLabel = entry[@"accessibilityLabel"];
1110
+ _toolbarButton.accessibilityIdentifier = entry[@"testID"];
1111
+ [_toolbarButton setImage:image forState:UIControlStateNormal];
1112
+ _loadedIconIdentity = identity;
1113
+ _loadedToolbarImage = image;
1114
+ _readyToolbarImage = nil;
1115
+ _readyToolbarIdentity = nil;
1116
+ _iconRequestPending = NO;
1117
+ [self setToolbarVisible:YES];
1118
+ [self setToolbarInteractive:YES];
1119
+ if (changed) [self setNeedsLayout];
1120
+ }
1121
+ - (void)applyReadyToolbarImage {
1122
+ if (!_readyToolbarImage || ![_readyToolbarIdentity isEqualToString:_iconIdentity] || ![self toolbarCanCommit]) return;
1123
+ [self commitToolbarImage:_readyToolbarImage identity:_readyToolbarIdentity];
1124
+ }
1125
+ - (void)hideFailedToolbar {
1126
+ [self setToolbarInteractive:NO];
1127
+ _toolbarKey = nil;
1128
+ _committedToolbarEntry = nil;
1129
+ [_toolbarButton setImage:nil forState:UIControlStateNormal];
1130
+ _toolbarButton.accessibilityLabel = nil;
1131
+ _toolbarButton.accessibilityIdentifier = nil;
1132
+ [self setToolbarVisible:NO];
1133
+ }
1134
+ - (void)updateToolbarForKey:(NSString *)key {
1135
+ if (![self ownsSettledToolbar]) { [self updateLegacyToolbarForKey:key]; return; }
1136
+ NSDictionary *toolbar = [_config[@"toolbar"] isKindOfClass:NSDictionary.class] ? _config[@"toolbar"] : nil;
1137
+ _toolbarReservedWidth = toolbar == nil ? 0 : 44;
1138
+ if (toolbar.count > 0) {
1139
+ _toolbarReservedWidth = 0;
1140
+ for (id value in toolbar.allValues) {
1141
+ NSDictionary *entry = [value isKindOfClass:NSDictionary.class] ? value : @{};
1142
+ CGFloat size = RNCPagerToolbarDimension(entry, @"size", 38, 1, 128);
1143
+ CGFloat content = RNCPagerToolbarDimension(entry, @"contentWidth", MIN(24, size), 0, size);
1144
+ CGFloat trailing = RNCPagerToolbarDimension(entry, @"trailingInset", 20, 0, 128);
1145
+ _toolbarReservedWidth = MAX(_toolbarReservedWidth, content + trailing);
1146
+ }
1147
+ }
1148
+ if (![self toolbarCanCommit]) { [self suspendToolbar]; return; }
1149
+ NSDictionary *entry = [toolbar[key ?: @""] isKindOfClass:NSDictionary.class] ? toolbar[key] : nil;
1150
+ NSString *name = [entry[@"iconName"] isKindOfClass:NSString.class] ? entry[@"iconName"] : nil;
1151
+ if (!name.length) {
1152
+ [self cancelToolbarRequest];
1153
+ _desiredToolbarKey = nil;
1154
+ _desiredToolbarEntry = nil;
1155
+ _toolbarKey = nil;
1156
+ _committedToolbarEntry = nil;
1157
+ [self setToolbarInteractive:NO];
1158
+ _toolbarButton.accessibilityLabel = nil;
1159
+ _toolbarButton.accessibilityIdentifier = nil;
1160
+ [self setToolbarVisible:NO];
1161
+ return;
1162
+ }
1163
+ _desiredToolbarKey = [key copy];
1164
+ _desiredToolbarEntry = [entry copy];
1165
+ CGFloat size = RNCPagerToolbarDimension(entry, @"size", 38, 1, 128);
1166
+ CGFloat iconSize = RNCPagerToolbarDimension(entry, @"iconSize", MIN(24, size), 1, size);
1167
+ NSString *color = [entry[@"iconColor"] isKindOfClass:NSString.class] ? entry[@"iconColor"] : @"#000000";
1168
+ CGFloat scale = self.window.screen.scale ?: UIScreen.mainScreen.scale;
1169
+ NSString *identity = [NSString stringWithFormat:@"%@|%@|%g|%g", name, color, scale, iconSize];
1170
+ if ([_loadedIconIdentity isEqualToString:identity] && _loadedToolbarImage) {
1171
+ [self cancelToolbarRequest];
1172
+ _iconIdentity = identity;
1173
+ [self commitToolbarImage:_loadedToolbarImage identity:identity];
1174
+ return;
1175
+ }
1176
+ if ([_iconIdentity isEqualToString:identity] && (_iconRequestPending || _readyToolbarImage)) {
1177
+ [self setToolbarInteractive:NO];
1178
+ [self applyReadyToolbarImage];
1179
+ return;
1180
+ }
1181
+ [self cancelToolbarRequest];
1182
+ _iconIdentity = identity;
1183
+ [self setToolbarInteractive:NO];
1184
+ if ([_failedIconIdentity isEqualToString:identity]) { [self hideFailedToolbar]; return; }
1185
+ NSObject *owner = [NSObject new];
1186
+ _iconOwner = owner;
1187
+ _iconRequestPending = YES;
1188
+ __weak __typeof__(self) weakSelf = self;
1189
+ [[self iconBridge] requestWithOwner:owner name:name color:color width:(NSInteger)ceil(iconSize * scale) height:(NSInteger)ceil(iconSize * scale) scale:scale complete:^(UIImage *image) {
1190
+ __strong __typeof__(weakSelf) self = weakSelf;
1191
+ if (!self || self->_iconOwner != owner || ![self->_iconIdentity isEqualToString:identity] || !self.window) return;
1192
+ self->_iconRequestPending = NO;
1193
+ if (!image) {
1194
+ self->_failedIconIdentity = identity;
1195
+ if ([self toolbarCanCommit]) [self hideFailedToolbar];
1196
+ return;
1197
+ }
1198
+ UIImage *scaled = image.CGImage ? [UIImage imageWithCGImage:image.CGImage scale:scale orientation:UIImageOrientationUp] : image;
1199
+ self->_readyToolbarImage = [scaled imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal];
1200
+ self->_readyToolbarIdentity = identity;
1201
+ [self applyReadyToolbarImage];
1202
+ }];
1203
+ }
1204
+ - (void)didMoveToWindow {
1205
+ [super didMoveToWindow];
1206
+ if (![self ownsSettledToolbar]) {
1207
+ if (_iconOwner) [[self iconBridge] cancelWithOwner:_iconOwner];
1208
+ _iconIdentity = nil;
1209
+ _loadedIconIdentity = nil;
1210
+ _iconRequestPending = NO;
1211
+ _readyToolbarImage = nil;
1212
+ _readyToolbarIdentity = nil;
1213
+ _failedIconIdentity = nil;
1214
+ _iconOwner = [NSObject new];
1215
+ _toolbarAnimationGeneration++;
1216
+ [_toolbarButton.layer removeAllAnimations];
1217
+ _toolbarVisible = NO;
1218
+ _toolbarButton.alpha = 0;
1219
+ _toolbarButton.hidden = YES;
1220
+ _toolbarButton.enabled = NO;
1221
+ _toolbarButton.userInteractionEnabled = NO;
1222
+ _toolbarButton.accessibilityElementsHidden = YES;
1223
+ _toolbarTouchKey = nil;
1224
+ _toolbarTouchStarted = NO;
1225
+ _toolbarButton.highlighted = NO;
1226
+ _toolbarButton.hovering = NO;
1227
+ [self updateToolbarForKey:_toolbarKey];
1228
+ return;
1229
+ }
1230
+ [self suspendToolbar];
1231
+ _toolbarPagerBusy = YES;
1232
+ _toolbarPageIndex = -1;
1233
+ _toolbarTouchStarted = NO;
1234
+ if (self.window && self.onToolbarNeedsSettlement) self.onToolbarNeedsSettlement();
1235
+ }
1236
+ - (void)dealloc { if (_iconOwner) [[self iconBridge] cancelWithOwner:_iconOwner]; }
1237
+ - (void)toolbarTouchDown {
1238
+ _toolbarTouchStarted = YES;
1239
+ _toolbarTouchKey = _toolbarButton.enabled && [self toolbarCanCommit] ? [_toolbarKey copy] : nil;
1240
+ }
1241
+ - (void)toolbarCancel {
1242
+ _toolbarTouchKey = nil;
1243
+ if (![self ownsSettledToolbar]) _toolbarTouchStarted = NO;
1244
+ }
1245
+ - (void)toolbarPress:(UIControl *)sender forEvent:(UIEvent *)event {
1246
+ if (![self ownsSettledToolbar]) {
1247
+ NSString *key = _toolbarTouchStarted ? _toolbarTouchKey : _toolbarKey;
1248
+ _toolbarTouchStarted = NO; _toolbarTouchKey = nil;
1249
+ if (key && [key isEqualToString:_toolbarKey] && !_toolbarButton.hidden && _toolbarButton.enabled && self.window && self.onToolbarPress)
1250
+ self.onToolbarPress(key, [_toolbarButton convertRect:_toolbarButton.bounds toView:self.window]);
1251
+ return;
1252
+ }
1253
+ BOOL pointer = event && event.type == UIEventTypeTouches;
1254
+ NSString *key = pointer ? (_toolbarTouchStarted ? _toolbarTouchKey : nil) : _toolbarKey;
1255
+ if (pointer) { _toolbarTouchStarted = NO; _toolbarTouchKey = nil; }
1256
+ BOOL selectedMatches = ![_config[@"controlled"] boolValue] || [key isEqual:_config[@"selectedKey"]];
1257
+ if (key && selectedMatches && [key isEqualToString:_toolbarKey] && [self toolbarCanCommit] &&
1258
+ !_toolbarButton.hidden && _toolbarButton.enabled && self.onToolbarPress) {
1259
+ self.onToolbarPress(key, [_toolbarButton convertRect:_toolbarButton.bounds toView:self.window]);
1260
+ }
1261
+ }
1262
+
696
1263
  - (void)setProgress:(CGFloat)progress
697
1264
  {
1265
+ _hasMappedPageProgress = NO;
1266
+ _nativeProgress = progress;
1267
+ _hasNativeProgress = YES;
698
1268
  if (_items.count == 0) return;
699
1269
  if (fabs(_progress - progress) >= 0.001) _shouldCenterSelectedItem = YES;
1270
+ if ([_config[@"controlled"] boolValue]) {
1271
+ NSArray *pageKeys = [_config[@"pageKeys"] isKindOfClass:NSArray.class] ? _config[@"pageKeys"] : @[];
1272
+ NSMutableArray<NSNumber *> *indices = [NSMutableArray new];
1273
+ NSSet *unique = [NSSet setWithArray:pageKeys];
1274
+ NSArray *itemKeys = [_items valueForKey:@"key"];
1275
+ BOOL valid = pageKeys.count > 0 && [pageKeys containsObject:_config[@"selectedKey"] ?: @""] && unique.count == pageKeys.count && [NSSet setWithArray:itemKeys].count == itemKeys.count;
1276
+ for (NSString *key in pageKeys) {
1277
+ NSUInteger index = [_items indexOfObjectPassingTest:^BOOL(NSDictionary *item, NSUInteger i, BOOL *stop) { return [item[@"key"] isEqual:key]; }];
1278
+ if (index == NSNotFound) { valid = NO; break; }
1279
+ [indices addObject:@(index)];
1280
+ }
1281
+ if (!valid) {
1282
+ NSUInteger selected = [_items indexOfObjectPassingTest:^BOOL(NSDictionary *item, NSUInteger i, BOOL *stop) { return [item[@"key"] isEqual:self->_config[@"selectedKey"]]; }];
1283
+ if (selected == NSNotFound) return;
1284
+ _progressSource = selected; _progressDestination = selected; _progressFraction = 0;
1285
+ _progress = selected;
1286
+ if (_indicatorFrames.count == _items.count) [self updatePresentation];
1287
+ return;
1288
+ }
1289
+ _hasMappedPageProgress = YES;
1290
+ CGFloat bounded = RNCClamp(progress, 0, pageKeys.count - 1);
1291
+ NSInteger lower = (NSInteger)floor(bounded);
1292
+ NSInteger upper = MIN(lower + 1, pageKeys.count - 1);
1293
+ _progressSource = indices[lower].integerValue;
1294
+ _progressDestination = indices[upper].integerValue;
1295
+ _progressFraction = bounded - lower;
1296
+ progress = _progressSource + (_progressDestination - _progressSource) * _progressFraction;
1297
+ }
700
1298
  _progress = RNCClamp(progress, 0, _items.count - 1);
701
1299
  if (_indicatorFrames.count == _items.count) [self updatePresentation];
702
1300
  }
703
1301
 
1302
+ - (NSInteger)visualFontOwnerIndex:(NSInteger)semanticIndex {
1303
+ if (_textVariant && [_config[@"controlled"] boolValue] && _hasMappedPageProgress) {
1304
+ return _progressFraction < 0.5 ? _progressSource : _progressDestination;
1305
+ }
1306
+ return semanticIndex;
1307
+ }
1308
+
704
1309
  - (void)updatePresentation
705
1310
  {
706
1311
  NSInteger count = _buttons.count;
@@ -718,11 +1323,22 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
718
1323
  _segmented ? CGRectGetHeight(fromFrame) : _indicatorHeight
719
1324
  );
720
1325
  _indicatorView.frame = indicatorFrame;
721
- _indicatorView.hidden = CGRectGetHeight(indicatorFrame) <= 0;
1326
+ _indicatorView.hidden = _textVariant || CGRectGetHeight(indicatorFrame) <= 0;
722
1327
 
723
1328
  NSInteger selectedIndex = (NSInteger)round(progress);
1329
+ if ([_config[@"controlled"] boolValue]) {
1330
+ NSUInteger selected = [_items indexOfObjectPassingTest:^BOOL(NSDictionary *item, NSUInteger i, BOOL *stop) { return [item[@"key"] isEqual:self->_config[@"selectedKey"]]; }];
1331
+ selectedIndex = selected == NSNotFound ? -1 : (NSInteger)selected;
1332
+ }
1333
+ NSInteger fontOwnerIndex = [self visualFontOwnerIndex:selectedIndex];
1334
+ NSString *key = selectedIndex >= 0 && selectedIndex < count ? _items[selectedIndex][@"key"] : nil;
1335
+ [self updateToolbarForKey:key];
724
1336
  [_buttons enumerateObjectsUsingBlock:^(UIButton *button, NSUInteger index, BOOL *stop) {
725
1337
  CGFloat emphasis = 1 - MIN(1, fabs(progress - index));
1338
+ if ([self->_config[@"controlled"] boolValue]) {
1339
+ emphasis = index == self->_progressSource ? 1 - self->_progressFraction : 0;
1340
+ if (index == self->_progressDestination) emphasis += self->_progressFraction;
1341
+ }
726
1342
  UIColor *color = RNCInterpolateColor(
727
1343
  self->_inactiveTextColor,
728
1344
  self->_activeTextColor,
@@ -730,6 +1346,11 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
730
1346
  self.traitCollection
731
1347
  );
732
1348
  [button setTitleColor:color forState:UIControlStateNormal];
1349
+ if (self->_textVariant) {
1350
+ button.titleLabel.font = index == fontOwnerIndex
1351
+ ? ([UIFont fontWithName:self->_config[@"activeFontFamily"] ?: @"" size:self->_fontSize] ?: [UIFont systemFontOfSize:self->_fontSize weight:UIFontWeightSemibold])
1352
+ : self.tabFont;
1353
+ }
733
1354
  button.accessibilityTraits = index == selectedIndex
734
1355
  ? UIAccessibilityTraitButton | UIAccessibilityTraitSelected
735
1356
  : UIAccessibilityTraitButton;
@@ -1534,6 +2155,24 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1534
2155
  - (BOOL)canPreventGestureRecognizer:(UIGestureRecognizer *)other { return YES; }
1535
2156
  @end
1536
2157
 
2158
+ @protocol RNCPagerPresentationProgressTarget <NSObject>
2159
+ - (void)sampleProgrammaticTabProgress:(CADisplayLink *)link;
2160
+ - (void)sampleToolbarSettlement:(CADisplayLink *)link;
2161
+ @end
2162
+
2163
+ @interface RNCPagerPresentationProgressProxy : NSObject
2164
+ @property (nonatomic, weak) id<RNCPagerPresentationProgressTarget> target;
2165
+ @property (nonatomic, assign) BOOL toolbarSettlement;
2166
+ @end
2167
+ @implementation RNCPagerPresentationProgressProxy
2168
+ - (void)tick:(CADisplayLink *)link {
2169
+ id<RNCPagerPresentationProgressTarget> target = self.target;
2170
+ if (target && self.toolbarSettlement) [target sampleToolbarSettlement:link];
2171
+ else if (target) [target sampleProgrammaticTabProgress:link];
2172
+ else [link invalidate];
2173
+ }
2174
+ @end
2175
+
1537
2176
  @interface RNCCollapsiblePagerViewComponentView () <
1538
2177
  RCTRNCCollapsiblePagerViewViewProtocol,
1539
2178
  UIPageViewControllerDataSource,
@@ -1542,8 +2181,12 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1542
2181
  UIGestureRecognizerDelegate,
1543
2182
  RCTMountingTransactionObserving,
1544
2183
  RNCNativeScrollerInsetCoordinator,
1545
- RNCPagerViewNestedOverlayObserver
2184
+ RNCPagerViewNestedOverlayObserver,
2185
+ RNCPagerPresentationProgressTarget
1546
2186
  >
2187
+ - (void)syncNativeToolbarState;
2188
+ - (void)cancelNativeToolbarSettlement;
2189
+ - (void)scheduleNativeToolbarSettlement;
1547
2190
  - (void)finishPagerScrollEmittingSelection:(BOOL)emitSelection;
1548
2191
  - (void)cancelPendingNativePagerScroll;
1549
2192
  - (void)completeTransitionOnNextRunLoopForGeneration:(NSUInteger)generation
@@ -1662,6 +2305,14 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1662
2305
  BOOL _nativeSmoothHeaderScrollEnabled;
1663
2306
  UIPanGestureRecognizer *_blockerGesture;
1664
2307
  BOOL _transitioning;
2308
+ BOOL _toolbarTransitionPending;
2309
+ CADisplayLink *_toolbarSettleDisplayLink;
2310
+ NSUInteger _toolbarSettleGeneration;
2311
+ NSUInteger _toolbarSettleTransition;
2312
+ NSInteger _toolbarSettlePage;
2313
+ CGFloat _toolbarSettleWidth;
2314
+ NSUInteger _toolbarSettleTicks;
2315
+ BOOL _toolbarSettleAttempted;
1665
2316
  BOOL _isPagerDragging;
1666
2317
  BOOL _directNativePagerEnabled;
1667
2318
  BOOL _pendingDirectNativePagerEnabled;
@@ -1672,6 +2323,10 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1672
2323
  NSUInteger _pendingNativePagerScrollGeneration;
1673
2324
  NSUInteger _pendingNativePagerScrollTransition;
1674
2325
  CGFloat _pendingNativePagerScrollTarget;
2326
+ CADisplayLink *_tabProgressDisplayLink;
2327
+ NSUInteger _tabProgressGeneration;
2328
+ NSUInteger _tabProgressTransition;
2329
+ CGFloat _tabProgressWidth;
1675
2330
  NSIndexSet *_programmaticPageMaskIndexes;
1676
2331
  NSMapTable<UIScrollView *, RNCCollapsiblePagerTransitionEdgeOwner *> *_programmaticScrollEdges;
1677
2332
  NSMutableArray<RNCCollapsiblePagerTransitionEdgeOwner *> *_programmaticScrollEdgeOwners;
@@ -1851,6 +2506,23 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1851
2506
  });
1852
2507
  }
1853
2508
  };
2509
+ _nativeTabBarView.onToolbarNeedsSettlement = ^{
2510
+ __strong __typeof__(weakSelf) self = weakSelf;
2511
+ if (!self) return;
2512
+ NSUInteger generation = self->_generation;
2513
+ dispatch_async(dispatch_get_main_queue(), ^{
2514
+ if (self->_generation == generation) [self syncNativeToolbarState];
2515
+ });
2516
+ };
2517
+ _nativeTabBarView.onToolbarPress = ^(NSString *key, CGRect rect) {
2518
+ __strong __typeof__(weakSelf) self = weakSelf;
2519
+ if (!self || self->_isBeingRecycled) return;
2520
+ const auto emitter = [self eventEmitter];
2521
+ if (emitter) emitter->onNativeToolbarPress({
2522
+ .key = std::string(key.UTF8String ?: ""),
2523
+ .anchorRect = {.x = rect.origin.x, .y = rect.origin.y, .width = rect.size.width, .height = rect.size.height}
2524
+ });
2525
+ };
1854
2526
  [_sharedHeaderHostView addSubview:_nativeTabBarView];
1855
2527
 
1856
2528
  _nativeSubHeaderView = [RNCCollapsiblePagerNativeSubHeaderView new];
@@ -1935,11 +2607,14 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1935
2607
  - (void)didMoveToWindow
1936
2608
  {
1937
2609
  [super didMoveToWindow];
2610
+ [self cancelNativeToolbarSettlement];
2611
+ if (self.window == nil) [self stopProgrammaticTabProgress];
1938
2612
  if (self.window == nil && _pendingNativePagerScrollCompletion != nil) {
1939
2613
  [self cancelPendingNativePagerScroll];
1940
2614
  _pagerScrollView.scrollEnabled = _scrollEnabled;
1941
2615
  [self finishPagerScrollEmittingSelection:NO];
1942
2616
  }
2617
+ [self syncNativeToolbarState];
1943
2618
  [self registerExternalOuterPager];
1944
2619
  BOOL externalHeightChanged = [self resolveExternalTopOverlay];
1945
2620
  if (self.window == nil) [self restoreScrollEdgeContentMasks];
@@ -2054,6 +2729,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2054
2729
  BOOL wasTransitioning = _externalOuterTransitioning;
2055
2730
  _externalOuterPageActive = active;
2056
2731
  _externalOuterTransitioning = transitioning;
2732
+ [self syncNativeToolbarState];
2057
2733
  if (transitioning && !wasTransitioning) {
2058
2734
  [self updateTopScrollEdgeForScrollView:nil];
2059
2735
  [self liftSharedHeadersForPagerTransition:@"outer"];
@@ -2418,8 +3094,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2418
3094
  [host addSubview:marker];
2419
3095
  return marker;
2420
3096
  };
2421
- owner.ownsTransitionHosts = allowExternalOverlay && _topOverlayView == nil &&
2422
- _externalTopOverlayTag > 0 && pageIndex != _currentIndex;
3097
+ owner.ownsTransitionHosts = pageIndex != _currentIndex;
2423
3098
  UIView *(^transitionHost)(UIView *) = ^UIView *(UIView *referenceHost) {
2424
3099
  CGRect frame = [referenceHost convertRect:referenceHost.bounds toView:owner.page];
2425
3100
  CGRect viewport = [scrollView convertRect:scrollView.bounds toView:owner.page];
@@ -2680,6 +3355,8 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2680
3355
 
2681
3356
  - (void)dealloc
2682
3357
  {
3358
+ [self cancelNativeToolbarSettlement];
3359
+ [self stopProgrammaticTabProgress];
2683
3360
  [self clearProgrammaticScrollEdges];
2684
3361
  [self updateTopScrollEdgeForScrollView:nil];
2685
3362
  [self cancelHeaderSettledCheck];
@@ -2734,6 +3411,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2734
3411
  }
2735
3412
  _hasPendingDirectNativePagerChange = NO;
2736
3413
  if (_directNativePagerEnabled == enabled) return;
3414
+ [self cancelNativeToolbarSettlement];
2737
3415
  [self cancelPendingNativePagerScroll];
2738
3416
 
2739
3417
  [self restoreScrollEdgeContentMasks];
@@ -2796,12 +3474,12 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2796
3474
  return width * MAX(0, physicalIndex);
2797
3475
  }
2798
3476
 
2799
- - (CGFloat)directPagerProgress
3477
+ - (CGFloat)directPagerProgressForOffset:(CGFloat)offset
2800
3478
  {
2801
3479
  CGFloat width = CGRectGetWidth(_pagerScrollView.bounds);
2802
3480
  if (width <= 0 || _pageControllers.count == 0) return _currentIndex;
2803
3481
  CGFloat physicalProgress = RNCClamp(
2804
- _pagerScrollView.contentOffset.x / width,
3482
+ offset / width,
2805
3483
  0,
2806
3484
  _pageControllers.count - 1
2807
3485
  );
@@ -2810,6 +3488,50 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2810
3488
  : _pageControllers.count - 1 - physicalProgress;
2811
3489
  }
2812
3490
 
3491
+ - (CGFloat)directPagerProgress {
3492
+ return [self directPagerProgressForOffset:_pagerScrollView.contentOffset.x];
3493
+ }
3494
+
3495
+ - (void)stopProgrammaticTabProgress {
3496
+ if (_tabProgressDisplayLink != nil && _tabProgressWidth > 0 && fabs(CGRectGetWidth(_pagerScrollView.bounds) - _tabProgressWidth) < 0.5) {
3497
+ CALayer *presentation = _pagerScrollView.layer.presentationLayer;
3498
+ if (presentation && std::isfinite(presentation.bounds.origin.x)) {
3499
+ [_nativeTabBarView setProgress:[self directPagerProgressForOffset:presentation.bounds.origin.x]];
3500
+ }
3501
+ }
3502
+ [_tabProgressDisplayLink invalidate];
3503
+ _tabProgressDisplayLink = nil;
3504
+ }
3505
+
3506
+ - (void)updateNativeTabProgress:(CGFloat)progress {
3507
+ if (_tabProgressDisplayLink == nil) [_nativeTabBarView setProgress:progress];
3508
+ }
3509
+
3510
+ - (void)sampleProgrammaticTabProgress:(CADisplayLink *)link {
3511
+ if (link != _tabProgressDisplayLink) { [link invalidate]; return; }
3512
+ if (self.window == nil || !_transitioning || ![_nativeTabBarView usesControlledTextProgress] ||
3513
+ _tabProgressGeneration != _generation || _tabProgressTransition != _transitionId ||
3514
+ !std::isfinite(CGRectGetWidth(_pagerScrollView.bounds)) || _tabProgressWidth <= 0 ||
3515
+ fabs(CGRectGetWidth(_pagerScrollView.bounds) - _tabProgressWidth) >= 0.5) {
3516
+ [self stopProgrammaticTabProgress];
3517
+ return;
3518
+ }
3519
+ CALayer *presentation = _pagerScrollView.layer.presentationLayer;
3520
+ if (presentation == nil || !std::isfinite(presentation.bounds.origin.x)) return;
3521
+ [_nativeTabBarView setProgress:[self directPagerProgressForOffset:presentation.bounds.origin.x]];
3522
+ }
3523
+
3524
+ - (void)startProgrammaticTabProgress {
3525
+ [self stopProgrammaticTabProgress];
3526
+ RNCPagerPresentationProgressProxy *proxy = [RNCPagerPresentationProgressProxy new];
3527
+ proxy.target = self;
3528
+ _tabProgressWidth = CGRectGetWidth(_pagerScrollView.bounds);
3529
+ _tabProgressGeneration = _generation;
3530
+ _tabProgressTransition = _transitionId;
3531
+ _tabProgressDisplayLink = [CADisplayLink displayLinkWithTarget:proxy selector:@selector(tick:)];
3532
+ [_tabProgressDisplayLink addToRunLoop:NSRunLoop.mainRunLoop forMode:NSRunLoopCommonModes];
3533
+ }
3534
+
2813
3535
  #pragma mark - Smooth shared header ownership
2814
3536
 
2815
3537
  - (void)restorePinnedStickyHeader
@@ -3068,8 +3790,81 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3068
3790
  }
3069
3791
  }
3070
3792
 
3793
+ - (void)cancelNativeToolbarSettlement {
3794
+ [_toolbarSettleDisplayLink invalidate];
3795
+ _toolbarSettleDisplayLink = nil;
3796
+ _toolbarSettleAttempted = NO;
3797
+ _toolbarSettleTicks = 0;
3798
+ }
3799
+
3800
+ - (void)scheduleNativeToolbarSettlement {
3801
+ CGFloat width = CGRectGetWidth(_pagerScrollView.bounds);
3802
+ BOOL sameAttempt = _toolbarSettleAttempted && _toolbarSettleGeneration == _generation &&
3803
+ _toolbarSettleTransition == _transitionId && _toolbarSettlePage == _currentIndex &&
3804
+ _toolbarSettleWidth == width;
3805
+ if (sameAttempt) return;
3806
+ [self cancelNativeToolbarSettlement];
3807
+ _toolbarSettleAttempted = YES;
3808
+ _toolbarSettleGeneration = _generation;
3809
+ _toolbarSettleTransition = _transitionId;
3810
+ _toolbarSettlePage = _currentIndex;
3811
+ _toolbarSettleWidth = width;
3812
+ RNCPagerPresentationProgressProxy *proxy = [RNCPagerPresentationProgressProxy new];
3813
+ proxy.target = self;
3814
+ proxy.toolbarSettlement = YES;
3815
+ _toolbarSettleDisplayLink = [CADisplayLink displayLinkWithTarget:proxy selector:@selector(tick:)];
3816
+ [_toolbarSettleDisplayLink addToRunLoop:NSRunLoop.mainRunLoop forMode:NSRunLoopCommonModes];
3817
+ }
3818
+
3819
+ - (void)sampleToolbarSettlement:(CADisplayLink *)link {
3820
+ if (link != _toolbarSettleDisplayLink) { [link invalidate]; return; }
3821
+ if (_toolbarSettleGeneration != _generation || _toolbarSettleTransition != _transitionId ||
3822
+ _toolbarSettlePage != _currentIndex ||
3823
+ _toolbarSettleWidth != CGRectGetWidth(_pagerScrollView.bounds) ||
3824
+ !self.window || _nativeTabBarView.window != self.window || ![_nativeTabBarView ownsSettledToolbar]) {
3825
+ [self cancelNativeToolbarSettlement];
3826
+ return;
3827
+ }
3828
+ ++_toolbarSettleTicks;
3829
+ [self syncNativeToolbarState];
3830
+ if (link == _toolbarSettleDisplayLink && _toolbarSettleTicks >= 2) {
3831
+ [link invalidate];
3832
+ _toolbarSettleDisplayLink = nil;
3833
+ }
3834
+ }
3835
+
3836
+ - (void)syncNativeToolbarState {
3837
+ BOOL logicalBusy = !self.window || _isBeingRecycled || !_hasAppliedInitialPage || _toolbarTransitionPending ||
3838
+ _transitioning || _isPagerDragging || _externalOuterTransitioning ||
3839
+ (_externalOuterPager != nil && !_externalOuterPageActive) || _needsSlotRebuild ||
3840
+ _pendingGoToIndex >= 0 || _transitionPageOffsets != nil || _pendingNativePagerScrollCompletion != nil ||
3841
+ _tabProgressDisplayLink != nil || _currentIndex < 0 || _currentIndex >= _pageControllers.count ||
3842
+ CGRectGetWidth(self.bounds) <= 0 || CGRectGetHeight(self.bounds) <= 0 ||
3843
+ _pagerScrollView.dragging || _pagerScrollView.decelerating;
3844
+ BOOL busy = logicalBusy || _pagerScrollView.tracking;
3845
+ if (!busy && _directNativePagerEnabled) {
3846
+ CALayer *presentation = _pagerScrollView.layer.presentationLayer;
3847
+ CGFloat offset = presentation ? presentation.bounds.origin.x : _pagerScrollView.contentOffset.x;
3848
+ busy = CGRectGetWidth(_pagerScrollView.bounds) <= 0 || !std::isfinite(offset) ||
3849
+ fabs(offset - [self directPagerOffsetForIndex:_currentIndex]) >= 0.5;
3850
+ } else if (!busy) {
3851
+ busy = _pageViewController.viewControllers.firstObject != _pageControllers[_currentIndex];
3852
+ }
3853
+ [_nativeTabBarView updateToolbarPagerIndex:_currentIndex busy:busy];
3854
+ if (busy && !logicalBusy && [_nativeTabBarView ownsSettledToolbar] &&
3855
+ _nativeTabBarView.window == self.window && !_nativeTabBarView.hidden &&
3856
+ CGRectGetWidth(_pagerScrollView.bounds) > 0) {
3857
+ [self scheduleNativeToolbarSettlement];
3858
+ } else {
3859
+ [self cancelNativeToolbarSettlement];
3860
+ }
3861
+ }
3862
+
3071
3863
  - (void)beginPageStateTransition
3072
3864
  {
3865
+ [self cancelNativeToolbarSettlement];
3866
+ _toolbarTransitionPending = YES;
3867
+ [_nativeTabBarView updateToolbarPagerIndex:-1 busy:YES];
3073
3868
  [self cancelPagerTop];
3074
3869
  if (!_nativeSmoothHeaderScrollEnabled || _headerHeight <= 0 || _transitionPageOffsets != nil) return;
3075
3870
  _transitionSourceDisplayBody = MAX(_observedScrollView.contentOffset.y + _observedScrollView.contentInset.top - _headerHeight, 0);
@@ -3444,7 +4239,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3444
4239
  animated:NO
3445
4240
  completion:nil];
3446
4241
  }
3447
- [_nativeTabBarView setProgress:_currentIndex];
4242
+ [self updateNativeTabProgress:_currentIndex];
3448
4243
  }
3449
4244
  if (_stickyHeaderView != nil) [_sharedHeaderHostView bringSubviewToFront:_stickyHeaderView];
3450
4245
  if (!_nativeTabBarView.hidden) [_sharedHeaderHostView bringSubviewToFront:_nativeTabBarView];
@@ -3462,6 +4257,9 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3462
4257
  - (void)layoutSubviews
3463
4258
  {
3464
4259
  [super layoutSubviews];
4260
+ if (_tabProgressDisplayLink != nil && fabs(CGRectGetWidth(self.bounds) - _tabProgressWidth) >= 0.5) {
4261
+ [self stopProgrammaticTabProgress];
4262
+ }
3465
4263
  void (^resizedPagerCompletion)(BOOL) = nil;
3466
4264
  NSInteger resizedPagerDestination = -1;
3467
4265
  NSUInteger resizedPagerGeneration = 0;
@@ -3504,7 +4302,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3504
4302
  animated:NO
3505
4303
  completion:nil];
3506
4304
  }
3507
- [_nativeTabBarView setProgress:_currentIndex];
4305
+ [self updateNativeTabProgress:_currentIndex];
3508
4306
  }
3509
4307
  // UIKit frame assignment is undefined while a view has a non-identity transform.
3510
4308
  // Restore transforms before settling page and shared-header frames.
@@ -3556,10 +4354,12 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3556
4354
  ) animated:NO];
3557
4355
  resizedPagerCompletion(YES);
3558
4356
  }
4357
+ [self syncNativeToolbarState];
3559
4358
  }
3560
4359
 
3561
4360
  - (void)prepareForRecycle
3562
4361
  {
4362
+ [self cancelNativeToolbarSettlement];
3563
4363
  [self cancelPendingNativePagerScroll];
3564
4364
  [_externalOuterPager removeNestedOverlayObserver:self];
3565
4365
  _externalOuterPager = nil;
@@ -3569,6 +4369,8 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3569
4369
  [self cancelHeaderSettledCheck];
3570
4370
  [self publishHeaderSettledAtTop:NO identityChanged:NO];
3571
4371
  _isBeingRecycled = YES;
4372
+ _toolbarTransitionPending = NO;
4373
+ [_nativeTabBarView updateToolbarPagerIndex:-1 busy:YES];
3572
4374
  _pendingTopOverlayAppearanceCommit = NO;
3573
4375
  [self syncScrollEdgeAppearance];
3574
4376
  _generation++;
@@ -3706,7 +4508,8 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3706
4508
  _needsPropsReapply = YES;
3707
4509
  _layoutDirection = @"ltr";
3708
4510
  [_nativeTabBarView updateItemsJSON:@"[]"];
3709
- [_nativeTabBarView setProgress:0];
4511
+ [_nativeTabBarView updateConfigJSON:@"{}"];
4512
+ [self updateNativeTabProgress:0];
3710
4513
  [_nativeSubHeaderView updateConfigJSON:@"{}"];
3711
4514
  _isBeingRecycled = NO;
3712
4515
  }
@@ -3835,6 +4638,9 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3835
4638
  _retainedPages = RCTNSStringFromString(newViewProps.retainedPages);
3836
4639
  [self setNeedsLayout];
3837
4640
  }
4641
+ if (_needsPropsReapply || oldViewProps.nativeTabBarConfig != newViewProps.nativeTabBarConfig) {
4642
+ [_nativeTabBarView updateConfigJSON:RCTNSStringFromString(newViewProps.nativeTabBarConfig)];
4643
+ }
3838
4644
  if (_needsPropsReapply || oldViewProps.nativeTabBarItems != newViewProps.nativeTabBarItems) {
3839
4645
  NSString *itemsJSON = RCTNSStringFromString(newViewProps.nativeTabBarItems);
3840
4646
  [_nativeTabBarView updateItemsJSON:itemsJSON];
@@ -3899,6 +4705,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3899
4705
  [self logHomeVisualGeometry:@"props-inset" scrollView:_observedScrollView];
3900
4706
  }
3901
4707
  _applyingTopOverlayInsetChange = NO;
4708
+ [self syncNativeToolbarState];
3902
4709
  if (!_nativeTabBarView.hidden) {
3903
4710
  [_nativeTabBarView.superview bringSubviewToFront:_nativeTabBarView];
3904
4711
  }
@@ -5324,6 +6131,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
5324
6131
 
5325
6132
  - (void)cancelPendingNativePagerScroll
5326
6133
  {
6134
+ [self stopProgrammaticTabProgress];
5327
6135
  if (_pendingNativePagerScrollCompletion == nil) return;
5328
6136
  _pendingNativePagerScrollCompletion = nil;
5329
6137
  _pendingNativePagerScrollGeneration = 0;
@@ -5342,6 +6150,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
5342
6150
  {
5343
6151
  [self cancelTopInsetReboundCheck];
5344
6152
  if (_isBeingRecycled || index < 0 || index >= _pageControllers.count) return;
6153
+ [self cancelNativeToolbarSettlement];
5345
6154
  _hasReceivedPageCommand = YES;
5346
6155
  _hasAppliedInitialPage = YES;
5347
6156
  if (_transitioning || _isPagerDragging) {
@@ -5363,6 +6172,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
5363
6172
  [self attachScrollObserverForCurrentPage];
5364
6173
  [self emitPageSelected:index];
5365
6174
  [self emitDiagnostics:@"page-selected"];
6175
+ [self syncNativeToolbarState];
5366
6176
  return;
5367
6177
  }
5368
6178
 
@@ -5391,13 +6201,14 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
5391
6201
  CGFloat targetOffset = [self directPagerOffsetForIndex:index];
5392
6202
  void (^animations)(void) = ^{
5393
6203
  [self->_pagerScrollView setContentOffset:CGPointMake(targetOffset, 0)];
5394
- [self->_nativeTabBarView setProgress:index];
6204
+ if (self->_tabProgressDisplayLink == nil) [self->_nativeTabBarView setProgress:index];
5395
6205
  [self updatePageStateTransitionProgress:index];
5396
6206
  };
5397
6207
  void (^completion)(BOOL) = ^(BOOL finished) {
5398
6208
  __strong __typeof__(weakSelf) self = weakSelf;
5399
6209
  if (self == nil || self->_generation != capturedGeneration ||
5400
6210
  self->_transitionId != capturedTransition || self->_isBeingRecycled) return;
6211
+ [self stopProgrammaticTabProgress];
5401
6212
  self->_pagerScrollView.scrollEnabled = self->_scrollEnabled;
5402
6213
  if (finished) {
5403
6214
  [self detachScrollObserver];
@@ -5433,7 +6244,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
5433
6244
  _pagerScrollView.scrollEnabled = NO;
5434
6245
  if (useNativeAnimatedScroll) {
5435
6246
  if (fabs(_pagerScrollView.contentOffset.x - targetOffset) <= 0.5) {
5436
- [_nativeTabBarView setProgress:index];
6247
+ [self updateNativeTabProgress:index];
5437
6248
  [self updatePageStateTransitionProgress:index];
5438
6249
  completion(YES);
5439
6250
  } else {
@@ -5444,6 +6255,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
5444
6255
  [_pagerScrollView setContentOffset:CGPointMake(targetOffset, 0) animated:YES];
5445
6256
  }
5446
6257
  } else {
6258
+ if ([_nativeTabBarView usesControlledTextProgress]) [self startProgrammaticTabProgress];
5447
6259
  [UIView animateWithDuration:0.28
5448
6260
  delay:0
5449
6261
  options:UIViewAnimationOptionCurveEaseInOut |
@@ -5508,10 +6320,12 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
5508
6320
  [self attachScrollObserverForCurrentPage];
5509
6321
  }
5510
6322
  [self drainPendingGoTo];
6323
+ if (!self->_transitioning && !self->_isPagerDragging && self->_pendingGoToIndex < 0) self->_toolbarTransitionPending = NO;
5511
6324
  if (!hadProgrammaticScrollEdges && !self->_transitioning && !self->_needsSlotRebuild) {
5512
6325
  [self attachScrollObserverForCurrentPage];
5513
6326
  }
5514
6327
  [self updateScrollEdgeContentMasks];
6328
+ [self syncNativeToolbarState];
5515
6329
  });
5516
6330
  }
5517
6331
 
@@ -5631,7 +6445,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
5631
6445
  [self directPagerOffsetForIndex:index],
5632
6446
  0
5633
6447
  ) animated:NO];
5634
- [_nativeTabBarView setProgress:index];
6448
+ [self updateNativeTabProgress:index];
5635
6449
  [self attachScrollObserverForCurrentPageAfterTransition:YES];
5636
6450
  if (emitSelection &&
5637
6451
  (_pendingGoToIndex < 0 || _pendingGoToIndex == index)) {
@@ -5682,7 +6496,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
5682
6496
  [self detachScrollObserver];
5683
6497
  _currentIndex = index;
5684
6498
  _destinationIndex = index;
5685
- [_nativeTabBarView setProgress:index];
6499
+ [self updateNativeTabProgress:index];
5686
6500
  [self attachScrollObserverForCurrentPageAfterTransition:YES];
5687
6501
  // A newer tab tap remains authoritative while its queued command runs.
5688
6502
  if (emitSelection &&
@@ -5743,7 +6557,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
5743
6557
  position = MAX(0, MIN(position, (NSInteger)_pageControllers.count - 1));
5744
6558
  [self updatePageStateTransitionProgress:progress];
5745
6559
  [self updateScrollEdgeContentMasks];
5746
- [_nativeTabBarView setProgress:progress];
6560
+ if (_tabProgressDisplayLink == nil) [self updateNativeTabProgress:progress];
5747
6561
  const auto emitter = [self eventEmitter];
5748
6562
  if (emitter) {
5749
6563
  emitter->onPageScroll({.position = (double)position, .offset = (double)offset});
@@ -5762,7 +6576,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
5762
6576
  }
5763
6577
  [self updatePageStateTransitionProgress:tabProgress];
5764
6578
  [self updateScrollEdgeContentMasks];
5765
- [_nativeTabBarView setProgress:tabProgress];
6579
+ [self updateNativeTabProgress:tabProgress];
5766
6580
  const auto emitter = [self eventEmitter];
5767
6581
  if (emitter) {
5768
6582
  emitter->onPageScroll({.position = (double)position, .offset = (double)offset});