Skip to content

Commit c7a7fc0

Browse files
committed
MMTabline: Add right-to-left (RTL) locale support
In RTL locales (e.g. Arabic, Hebrew), macOS lays everything out in the flipped direction, including most UI elements and native tabs. This change makes sure MacVim tabs will obey the same convention and behave intuitively in such locales. The buttons and UI elements in MMTab/MMTabline already automatically get flipped. However, the logic of handling the tabs placements, scrolling, and drag-and-drop use manual calculations and need to be fixed up. In order to keep scrolling stable, and for tabs animation to look correct and the same as the left-to-right, we simply flip the frames we use for tabs layout, by starting from 0 in X coordinate, and grow towards the negative range. This helps keep most of the logic the same while only needing to apply the X-flip adjustment in a couple places. Also, as a minor adjustment, make the default widths of the tab just a bit wider.
1 parent 62f5e1a commit c7a7fc0

File tree

4 files changed

+150
-40
lines changed

4 files changed

+150
-40
lines changed

src/MacVim/MMAppController.m

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,8 +174,8 @@ + (void)registerDefaults
174174

175175
NSDictionary *macvimDefaults = [NSDictionary dictionaryWithObjectsAndKeys:
176176
[NSNumber numberWithBool:NO], MMNoWindowKey,
177-
[NSNumber numberWithInt:120], MMTabMinWidthKey,
178-
[NSNumber numberWithInt:200], MMTabOptimumWidthKey,
177+
[NSNumber numberWithInt:130], MMTabMinWidthKey,
178+
[NSNumber numberWithInt:210], MMTabOptimumWidthKey,
179179
[NSNumber numberWithBool:YES], MMShowAddTabButtonKey,
180180
[NSNumber numberWithBool:NO], MMShowTabScrollButtonsKey,
181181
[NSNumber numberWithInt:2], MMTextInsetLeftKey,

src/MacVim/MMTabline/MMTabline.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,8 @@
5555
- (void)selectTabAtIndex:(NSInteger)index;
5656
- (MMTab *)tabAtIndex:(NSInteger)index;
5757
- (void)scrollTabToVisibleAtIndex:(NSInteger)index;
58-
- (void)scrollLeftOneTab;
59-
- (void)scrollRightOneTab;
58+
- (void)scrollBackwardOneTab;
59+
- (void)scrollForwardOneTab;
6060
- (void)setTablineSelBackground:(NSColor *)back foreground:(NSColor *)fore;
6161

6262
@end

src/MacVim/MMTabline/MMTabline.m

Lines changed: 144 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@
88
CGFloat remainder;
99
} TabWidth;
1010

11-
const CGFloat OptimumTabWidth = 220;
12-
const CGFloat MinimumTabWidth = 100;
13-
const CGFloat TabOverlap = 6;
14-
const CGFloat ScrollOneTabAllowance = 0.25; // If we are showing 75+% of the tab, consider it to be fully shown when deciding whether to scroll to next tab.
11+
static const CGFloat OptimumTabWidth = 200;
12+
static const CGFloat MinimumTabWidth = 100;
13+
static const CGFloat TabOverlap = 6;
14+
static const CGFloat ScrollOneTabAllowance = 0.25; // If we are showing 75+% of the tab, consider it to be fully shown when deciding whether to scroll to next tab.
1515

1616
static MMHoverButton* MakeHoverButton(MMTabline *tabline, MMHoverButtonImage imageType, NSString *tooltip, SEL action, BOOL continuous) {
1717
MMHoverButton *button = [MMHoverButton new];
@@ -44,8 +44,8 @@ @implementation MMTabline
4444
CGFloat _xOffsetForDrag;
4545
NSInteger _initialDraggedTabIndex;
4646
NSInteger _finalDraggedTabIndex;
47-
MMHoverButton *_leftScrollButton;
48-
MMHoverButton *_rightScrollButton;
47+
MMHoverButton *_backwardScrollButton;
48+
MMHoverButton *_forwardScrollButton;
4949
id _scrollWheelEventMonitor;
5050
}
5151

@@ -82,23 +82,40 @@ - (instancetype)initWithFrame:(NSRect)frameRect
8282
_scrollView.documentView = _tabsContainer;
8383
[self addSubview:_scrollView];
8484

85-
_addTabButton = MakeHoverButton(self, MMHoverButtonImageAddTab, NSLocalizedString(@"create-new-tab-button", @"Create a new tab button"), @selector(addTabAtEnd), NO);
86-
_leftScrollButton = MakeHoverButton(self, MMHoverButtonImageScrollLeft, NSLocalizedString(@"scroll-tabs-backward", @"Scroll backward button in tabs line"), @selector(scrollLeftOneTab), YES);
87-
_rightScrollButton = MakeHoverButton(self, MMHoverButtonImageScrollRight, NSLocalizedString(@"scroll-tabs-forward", @"Scroll forward button in tabs line"), @selector(scrollRightOneTab), YES);
88-
89-
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:[_leftScrollButton][_rightScrollButton]-5-[_scrollView]-5-[_addTabButton]" options:NSLayoutFormatAlignAllCenterY metrics:nil views:NSDictionaryOfVariableBindings(_scrollView, _leftScrollButton, _rightScrollButton, _addTabButton)]];
85+
_addTabButton = MakeHoverButton(
86+
self,
87+
MMHoverButtonImageAddTab,
88+
NSLocalizedString(@"create-new-tab-button", @"Create a new tab button"),
89+
@selector(addTabAtEnd),
90+
NO);
91+
_backwardScrollButton = MakeHoverButton(
92+
self,
93+
[self useRightToLeft] ? MMHoverButtonImageScrollRight : MMHoverButtonImageScrollLeft,
94+
NSLocalizedString(@"scroll-tabs-backward", @"Scroll backward button in tabs line"),
95+
@selector(scrollBackwardOneTab),
96+
YES);
97+
_forwardScrollButton = MakeHoverButton(
98+
self,
99+
[self useRightToLeft] ? MMHoverButtonImageScrollLeft : MMHoverButtonImageScrollRight,
100+
NSLocalizedString(@"scroll-tabs-forward", @"Scroll forward button in tabs line"),
101+
@selector(scrollForwardOneTab),
102+
YES);
103+
104+
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:[_backwardScrollButton][_forwardScrollButton]-5-[_scrollView]-5-[_addTabButton]" options:NSLayoutFormatAlignAllCenterY metrics:nil views:NSDictionaryOfVariableBindings(_scrollView, _backwardScrollButton, _forwardScrollButton, _addTabButton)]];
90105
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[_scrollView]|" options:0 metrics:nil views:@{@"_scrollView":_scrollView}]];
91106

92-
_tabScrollButtonsLeadingConstraint = [NSLayoutConstraint constraintWithItem:_leftScrollButton attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeLeading multiplier:1 constant:5];
107+
_tabScrollButtonsLeadingConstraint = [NSLayoutConstraint constraintWithItem:_backwardScrollButton attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeLeading multiplier:1 constant:5];
93108
[self addConstraint:_tabScrollButtonsLeadingConstraint];
94109

95110
_addTabButtonTrailingConstraint = [NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationEqual toItem:_addTabButton attribute:NSLayoutAttributeTrailing multiplier:1 constant:5];
96111
[self addConstraint:_addTabButtonTrailingConstraint];
97112

98113
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didScroll:) name:NSViewBoundsDidChangeNotification object:_scrollView.contentView];
114+
if ([self useRightToLeft]) {
115+
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateTabsContainerBoundsForRTL:) name:NSViewFrameDidChangeNotification object:_tabsContainer];
116+
}
99117

100118
[self addScrollWheelMonitor];
101-
102119
}
103120
return self;
104121
}
@@ -194,7 +211,7 @@ - (void)setShowsTabScrollButtons:(BOOL)showsTabScrollButtons
194211
// (see -drawRect: in MMTab.m).
195212
if (_showsTabScrollButtons != showsTabScrollButtons) {
196213
_showsTabScrollButtons = showsTabScrollButtons;
197-
_tabScrollButtonsLeadingConstraint.constant = showsTabScrollButtons ? 5 : -((NSWidth(_leftScrollButton.frame) * 2) + 5 + MMTabShadowBlurRadius);
214+
_tabScrollButtonsLeadingConstraint.constant = showsTabScrollButtons ? 5 : -((NSWidth(_backwardScrollButton.frame) * 2) + 5 + MMTabShadowBlurRadius);
198215
}
199216
}
200217

@@ -244,8 +261,8 @@ - (void)setTablineSelFgColor:(NSColor *)color
244261
{
245262
_tablineSelFgColor = color;
246263
_addTabButton.fgColor = color;
247-
_leftScrollButton.fgColor = color;
248-
_rightScrollButton.fgColor = color;
264+
_backwardScrollButton.fgColor = color;
265+
_forwardScrollButton.fgColor = color;
249266
for (MMTab *tab in _tabs) tab.state = tab.state;
250267
}
251268

@@ -280,6 +297,7 @@ - (NSInteger)addTabAtIndex:(NSInteger)index
280297
NSRect frame = _tabsContainer.bounds;
281298
frame.size.width = index == _tabs.count ? t.width + t.remainder : t.width;
282299
frame.origin.x = index * (t.width - TabOverlap);
300+
frame = [self flipRectRTL:frame];
283301
MMTab *newTab = [[MMTab alloc] initWithFrame:frame tabline:self];
284302

285303
[_tabs insertObject:newTab atIndex:index];
@@ -383,6 +401,7 @@ - (void)updateTabsByTags:(NSInteger *)tags len:(NSUInteger)len delayTabResize:(B
383401
NSRect frame = _tabsContainer.bounds;
384402
frame.size.width = i == (len - 1) ? t.width + t.remainder : t.width;
385403
frame.origin.x = i * (t.width - TabOverlap);
404+
frame = [self flipRectRTL:frame];
386405
MMTab *newTab = [[MMTab alloc] initWithFrame:frame tabline:self];
387406
newTab.tag = tag;
388407
[newTabs addObject:newTab];
@@ -533,19 +552,35 @@ - (void)setTablineSelBackground:(NSColor *)back foreground:(NSColor *)fore
533552

534553
#pragma mark - Helpers
535554

536-
NSComparisonResult SortTabsForZOrder(MMTab *tab1, MMTab *tab2, void *draggedTab)
555+
NSComparisonResult SortTabsForZOrder(MMTab *tab1, MMTab *tab2, void *draggedTab, BOOL rtl)
537556
{ // Z-order, highest to lowest: dragged, selected, hovered, rightmost
538557
if (tab1 == (__bridge MMTab *)draggedTab) return NSOrderedDescending;
539558
if (tab2 == (__bridge MMTab *)draggedTab) return NSOrderedAscending;
540559
if (tab1.state == MMTabStateSelected) return NSOrderedDescending;
541560
if (tab2.state == MMTabStateSelected) return NSOrderedAscending;
542561
if (tab1.state == MMTabStateUnselectedHover) return NSOrderedDescending;
543562
if (tab2.state == MMTabStateUnselectedHover) return NSOrderedAscending;
544-
if (NSMinX(tab1.frame) < NSMinX(tab2.frame)) return NSOrderedAscending;
545-
if (NSMinX(tab1.frame) > NSMinX(tab2.frame)) return NSOrderedDescending;
563+
if (rtl) {
564+
if (NSMinX(tab1.frame) > NSMinX(tab2.frame)) return NSOrderedAscending;
565+
if (NSMinX(tab1.frame) < NSMinX(tab2.frame)) return NSOrderedDescending;
566+
} else {
567+
if (NSMinX(tab1.frame) < NSMinX(tab2.frame)) return NSOrderedAscending;
568+
if (NSMinX(tab1.frame) > NSMinX(tab2.frame)) return NSOrderedDescending;
569+
}
546570
return NSOrderedSame;
547571
}
548572

573+
NSComparisonResult SortTabsForZOrderLTR(MMTab *tab1, MMTab *tab2, void *draggedTab)
574+
{
575+
return SortTabsForZOrder(tab1, tab2, draggedTab, NO);
576+
}
577+
578+
579+
NSComparisonResult SortTabsForZOrderRTL(MMTab *tab1, MMTab *tab2, void *draggedTab)
580+
{
581+
return SortTabsForZOrder(tab1, tab2, draggedTab, YES);
582+
}
583+
549584
- (TabWidth)tabWidthForTabs:(NSInteger)numTabs
550585
{
551586
// Each tab (except the first) overlaps the previous tab by TabOverlap
@@ -620,7 +655,13 @@ - (void)fixupCloseButtons
620655

621656
- (void)fixupTabZOrder
622657
{
623-
[_tabsContainer sortSubviewsUsingFunction:SortTabsForZOrder context:(__bridge void *)(_draggedTab)];
658+
if ([self useRightToLeft]) {
659+
[_tabsContainer sortSubviewsUsingFunction:SortTabsForZOrderRTL
660+
context:(__bridge void *)(_draggedTab)];
661+
} else {
662+
[_tabsContainer sortSubviewsUsingFunction:SortTabsForZOrderLTR
663+
context:(__bridge void *)(_draggedTab)];
664+
}
624665
}
625666

626667
- (void)fixupLayoutWithAnimation:(BOOL)shouldAnimate delayResize:(BOOL)delayResize
@@ -656,6 +697,7 @@ - (void)fixupLayoutWithAnimation:(BOOL)shouldAnimate delayResize:(BOOL)delayResi
656697
frame.size.width = i == _tabs.count - 1 ? t.width + t.remainder : t.width;
657698
frame.origin.x = i != 0 ? i * (t.width - TabOverlap) : 0;
658699
}
700+
frame = [self flipRectRTL:frame];
659701
if (shouldAnimate) {
660702
[NSAnimationContext runAnimationGroup:^(NSAnimationContext * _Nonnull context) {
661703
context.allowsImplicitAnimation = YES;
@@ -673,8 +715,20 @@ - (void)fixupLayoutWithAnimation:(BOOL)shouldAnimate delayResize:(BOOL)delayResi
673715
NSRect frame = _tabsContainer.frame;
674716
frame.size.width = t.width * _tabs.count - TabOverlap * (_tabs.count - 1);
675717
frame.size.width = NSWidth(frame) < NSWidth(_scrollView.frame) ? NSWidth(_scrollView.frame) : NSWidth(frame);
676-
if (shouldAnimate) _tabsContainer.animator.frame = frame;
677-
else _tabsContainer.frame = frame;
718+
const BOOL sizeDecreasing = NSWidth(frame) < NSWidth(_tabsContainer.frame);
719+
if ([self useRightToLeft]) {
720+
// In RTL mode we flip the X coords and grow from 0 to negative.
721+
// See updateTabsContainerBoundsForRTL which auto-updates the
722+
// bounds to match the frame.
723+
frame.origin.x = -NSWidth(frame);
724+
}
725+
if (shouldAnimate && sizeDecreasing) {
726+
// Need to animate to make sure we don't immediately get clamped by
727+
// the new size if we are already scrolled all the way to the back.
728+
_tabsContainer.animator.frame = frame;
729+
} else {
730+
_tabsContainer.frame = frame;
731+
}
678732
[self updateTabScrollButtonsEnabledState];
679733
}
680734
}
@@ -684,6 +738,41 @@ - (void)fixupLayoutWithAnimation:(BOOL)shouldAnimate
684738
[self fixupLayoutWithAnimation:shouldAnimate delayResize:NO];
685739
}
686740

741+
#pragma mark - Right-to-left (RTL) support
742+
743+
- (BOOL)useRightToLeft
744+
{
745+
// MMTabs support RTL locales. In such locales user interface items are
746+
// laid out from right to left. The layout of hover buttons and views are
747+
// automatically flipped by AppKit, but we need to handle this manually in
748+
// the tab placement logic since that is custom logic.
749+
return self.userInterfaceLayoutDirection == NSUserInterfaceLayoutDirectionRightToLeft;
750+
}
751+
752+
- (void)updateTabsContainerBoundsForRTL:(NSNotification *)notification
753+
{
754+
// In RTL mode, we grow the tabs container to the left. We want to preserve
755+
// stability of the scroll view's bounds, and also have the tabs animate
756+
// correctly. To do this, we have to make sure the container bounds matches
757+
// the frame at all times. This "cancels out" the negative X offsets with
758+
// each other and ease calculations.
759+
// E.g. an MMTab with origin (-100,0) inside the _tabsContainer coordinate
760+
// space will actually be (-100,0) in the scroll view as well.
761+
// In LTR mode we don't need this, since _tabsContainer's origin is always
762+
// at (0,0).
763+
_tabsContainer.bounds = _tabsContainer.frame;
764+
}
765+
766+
- (NSRect)flipRectRTL:(NSRect)frame
767+
{
768+
if (![self useRightToLeft])
769+
return frame;
770+
// In right-to-left mode, we flip the X coordinates for all the tabs so
771+
// they start at 0 and grow in the negative direction.
772+
frame.origin.x = -NSMaxX(frame);
773+
return frame;
774+
}
775+
687776
#pragma mark - Mouse
688777

689778
- (void)updateTrackingAreas
@@ -796,9 +885,15 @@ - (void)mouseDragged:(NSEvent *)event
796885
[self fixupTabZOrder];
797886
[_draggedTab setFrameOrigin:NSMakePoint(mouse.x - _xOffsetForDrag, 0)];
798887
MMTab *selectedTab = _selectedTabIndex == -1 ? nil : _tabs[_selectedTabIndex];
888+
const BOOL rightToLeft = [self useRightToLeft];
799889
[_tabs sortWithOptions:NSSortStable usingComparator:^NSComparisonResult(MMTab *t1, MMTab *t2) {
800-
if (NSMinX(t1.frame) <= NSMinX(t2.frame)) return NSOrderedAscending;
801-
if (NSMinX(t1.frame) > NSMinX(t2.frame)) return NSOrderedDescending;
890+
if (rightToLeft) {
891+
if (NSMaxX(t1.frame) >= NSMaxX(t2.frame)) return NSOrderedAscending;
892+
if (NSMaxX(t1.frame) < NSMaxX(t2.frame)) return NSOrderedDescending;
893+
} else {
894+
if (NSMinX(t1.frame) <= NSMinX(t2.frame)) return NSOrderedAscending;
895+
if (NSMinX(t1.frame) > NSMinX(t2.frame)) return NSOrderedDescending;
896+
}
802897
return NSOrderedSame;
803898
}];
804899
_selectedTabIndex = _selectedTabIndex == -1 ? -1 : [_tabs indexOfObject:selectedTab];
@@ -820,11 +915,18 @@ - (void)updateTabScrollButtonsEnabledState
820915
// on either side of _scrollView.
821916
NSRect clipBounds = _scrollView.contentView.bounds;
822917
if (NSWidth(_tabsContainer.frame) <= NSWidth(clipBounds)) {
823-
_leftScrollButton.enabled = NO;
824-
_rightScrollButton.enabled = NO;
918+
_backwardScrollButton.enabled = NO;
919+
_forwardScrollButton.enabled = NO;
825920
} else {
826-
_leftScrollButton.enabled = clipBounds.origin.x > 0;
827-
_rightScrollButton.enabled = clipBounds.origin.x + NSWidth(clipBounds) < NSMaxX(_tabsContainer.frame);
921+
BOOL scrollLeftEnabled = NSMinX(clipBounds) > NSMinX(_tabsContainer.frame);
922+
BOOL scrollRightEnabled = NSMaxX(clipBounds) < NSMaxX(_tabsContainer.frame);
923+
if ([self useRightToLeft]) {
924+
_backwardScrollButton.enabled = scrollRightEnabled;
925+
_forwardScrollButton.enabled = scrollLeftEnabled;
926+
} else {
927+
_backwardScrollButton.enabled = scrollLeftEnabled;
928+
_forwardScrollButton.enabled = scrollRightEnabled;
929+
}
828930
}
829931
}
830932

@@ -874,29 +976,37 @@ - (void)scrollTabToVisibleAtIndex:(NSInteger)index
874976
}
875977
}
876978

877-
- (void)scrollLeftOneTab
979+
- (void)scrollBackwardOneTab
878980
{
879981
NSRect clipBounds = _scrollView.contentView.animator.bounds;
880982
for (NSInteger i = _tabs.count - 1; i >= 0; i--) {
881983
NSRect tabFrame = _tabs[i].frame;
882984
if (!NSContainsRect(clipBounds, tabFrame)) {
883-
CGFloat allowance = i == 0 ? 0 : NSWidth(tabFrame) * ScrollOneTabAllowance;
884-
if (NSMinX(tabFrame) + allowance < NSMinX(clipBounds)) {
985+
const CGFloat allowance = (i == 0) ?
986+
0 : NSWidth(tabFrame) * ScrollOneTabAllowance;
987+
const BOOL outOfBounds = [self useRightToLeft] ?
988+
NSMaxX(tabFrame) - allowance > NSMaxX(clipBounds) :
989+
NSMinX(tabFrame) + allowance < NSMinX(clipBounds);
990+
if (outOfBounds) {
885991
[self scrollTabToVisibleAtIndex:i];
886992
break;
887993
}
888994
}
889995
}
890996
}
891997

892-
- (void)scrollRightOneTab
998+
- (void)scrollForwardOneTab
893999
{
8941000
NSRect clipBounds = _scrollView.contentView.animator.bounds;
8951001
for (NSInteger i = 0; i < _tabs.count; i++) {
8961002
NSRect tabFrame = _tabs[i].frame;
8971003
if (!NSContainsRect(clipBounds, tabFrame)) {
898-
CGFloat allowance = i == _tabs.count - 1 ? 0 : NSWidth(tabFrame) * ScrollOneTabAllowance;
899-
if (NSMaxX(tabFrame) - allowance > NSMaxX(clipBounds)) {
1004+
const CGFloat allowance = (i == _tabs.count - 1) ?
1005+
0 : NSWidth(tabFrame) * ScrollOneTabAllowance;
1006+
const BOOL outOfBounds = [self useRightToLeft] ?
1007+
NSMinX(tabFrame) + allowance < NSMinX(clipBounds) :
1008+
NSMaxX(tabFrame) - allowance > NSMaxX(clipBounds);
1009+
if (outOfBounds) {
9001010
[self scrollTabToVisibleAtIndex:i];
9011011
break;
9021012
}

src/MacVim/MMVimView.m

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -264,12 +264,12 @@ - (IBAction)scrollToCurrentTab:(id)sender
264264

265265
- (IBAction)scrollBackwardOneTab:(id)sender
266266
{
267-
[tabline scrollLeftOneTab];
267+
[tabline scrollBackwardOneTab];
268268
}
269269

270270
- (IBAction)scrollForwardOneTab:(id)sender
271271
{
272-
[tabline scrollRightOneTab];
272+
[tabline scrollForwardOneTab];
273273
}
274274

275275
- (void)showTabline:(BOOL)on

0 commit comments

Comments
 (0)