Skip to content

Commit 3f8e405

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 3f8e405

File tree

4 files changed

+152
-40
lines changed

4 files changed

+152
-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: 146 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,9 +655,17 @@ - (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

667+
/// The main layout function that calculates the tab positions and animate them
668+
/// accordingly. Call this every time tabs have been added/removed/moved.
626669
- (void)fixupLayoutWithAnimation:(BOOL)shouldAnimate delayResize:(BOOL)delayResize
627670
{
628671
if (!self.useAnimation)
@@ -656,6 +699,7 @@ - (void)fixupLayoutWithAnimation:(BOOL)shouldAnimate delayResize:(BOOL)delayResi
656699
frame.size.width = i == _tabs.count - 1 ? t.width + t.remainder : t.width;
657700
frame.origin.x = i != 0 ? i * (t.width - TabOverlap) : 0;
658701
}
702+
frame = [self flipRectRTL:frame];
659703
if (shouldAnimate) {
660704
[NSAnimationContext runAnimationGroup:^(NSAnimationContext * _Nonnull context) {
661705
context.allowsImplicitAnimation = YES;
@@ -673,8 +717,20 @@ - (void)fixupLayoutWithAnimation:(BOOL)shouldAnimate delayResize:(BOOL)delayResi
673717
NSRect frame = _tabsContainer.frame;
674718
frame.size.width = t.width * _tabs.count - TabOverlap * (_tabs.count - 1);
675719
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;
720+
const BOOL sizeDecreasing = NSWidth(frame) < NSWidth(_tabsContainer.frame);
721+
if ([self useRightToLeft]) {
722+
// In RTL mode we flip the X coords and grow from 0 to negative.
723+
// See updateTabsContainerBoundsForRTL which auto-updates the
724+
// bounds to match the frame.
725+
frame.origin.x = -NSWidth(frame);
726+
}
727+
if (shouldAnimate && sizeDecreasing) {
728+
// Need to animate to make sure we don't immediately get clamped by
729+
// the new size if we are already scrolled all the way to the back.
730+
_tabsContainer.animator.frame = frame;
731+
} else {
732+
_tabsContainer.frame = frame;
733+
}
678734
[self updateTabScrollButtonsEnabledState];
679735
}
680736
}
@@ -684,6 +740,41 @@ - (void)fixupLayoutWithAnimation:(BOOL)shouldAnimate
684740
[self fixupLayoutWithAnimation:shouldAnimate delayResize:NO];
685741
}
686742

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

689780
- (void)updateTrackingAreas
@@ -796,9 +887,15 @@ - (void)mouseDragged:(NSEvent *)event
796887
[self fixupTabZOrder];
797888
[_draggedTab setFrameOrigin:NSMakePoint(mouse.x - _xOffsetForDrag, 0)];
798889
MMTab *selectedTab = _selectedTabIndex == -1 ? nil : _tabs[_selectedTabIndex];
890+
const BOOL rightToLeft = [self useRightToLeft];
799891
[_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;
892+
if (rightToLeft) {
893+
if (NSMaxX(t1.frame) >= NSMaxX(t2.frame)) return NSOrderedAscending;
894+
if (NSMaxX(t1.frame) < NSMaxX(t2.frame)) return NSOrderedDescending;
895+
} else {
896+
if (NSMinX(t1.frame) <= NSMinX(t2.frame)) return NSOrderedAscending;
897+
if (NSMinX(t1.frame) > NSMinX(t2.frame)) return NSOrderedDescending;
898+
}
802899
return NSOrderedSame;
803900
}];
804901
_selectedTabIndex = _selectedTabIndex == -1 ? -1 : [_tabs indexOfObject:selectedTab];
@@ -820,11 +917,18 @@ - (void)updateTabScrollButtonsEnabledState
820917
// on either side of _scrollView.
821918
NSRect clipBounds = _scrollView.contentView.bounds;
822919
if (NSWidth(_tabsContainer.frame) <= NSWidth(clipBounds)) {
823-
_leftScrollButton.enabled = NO;
824-
_rightScrollButton.enabled = NO;
920+
_backwardScrollButton.enabled = NO;
921+
_forwardScrollButton.enabled = NO;
825922
} else {
826-
_leftScrollButton.enabled = clipBounds.origin.x > 0;
827-
_rightScrollButton.enabled = clipBounds.origin.x + NSWidth(clipBounds) < NSMaxX(_tabsContainer.frame);
923+
BOOL scrollLeftEnabled = NSMinX(clipBounds) > NSMinX(_tabsContainer.frame);
924+
BOOL scrollRightEnabled = NSMaxX(clipBounds) < NSMaxX(_tabsContainer.frame);
925+
if ([self useRightToLeft]) {
926+
_backwardScrollButton.enabled = scrollRightEnabled;
927+
_forwardScrollButton.enabled = scrollLeftEnabled;
928+
} else {
929+
_backwardScrollButton.enabled = scrollLeftEnabled;
930+
_forwardScrollButton.enabled = scrollRightEnabled;
931+
}
828932
}
829933
}
830934

@@ -874,29 +978,37 @@ - (void)scrollTabToVisibleAtIndex:(NSInteger)index
874978
}
875979
}
876980

877-
- (void)scrollLeftOneTab
981+
- (void)scrollBackwardOneTab
878982
{
879983
NSRect clipBounds = _scrollView.contentView.animator.bounds;
880984
for (NSInteger i = _tabs.count - 1; i >= 0; i--) {
881985
NSRect tabFrame = _tabs[i].frame;
882986
if (!NSContainsRect(clipBounds, tabFrame)) {
883-
CGFloat allowance = i == 0 ? 0 : NSWidth(tabFrame) * ScrollOneTabAllowance;
884-
if (NSMinX(tabFrame) + allowance < NSMinX(clipBounds)) {
987+
const CGFloat allowance = (i == 0) ?
988+
0 : NSWidth(tabFrame) * ScrollOneTabAllowance;
989+
const BOOL outOfBounds = [self useRightToLeft] ?
990+
NSMaxX(tabFrame) - allowance > NSMaxX(clipBounds) :
991+
NSMinX(tabFrame) + allowance < NSMinX(clipBounds);
992+
if (outOfBounds) {
885993
[self scrollTabToVisibleAtIndex:i];
886994
break;
887995
}
888996
}
889997
}
890998
}
891999

892-
- (void)scrollRightOneTab
1000+
- (void)scrollForwardOneTab
8931001
{
8941002
NSRect clipBounds = _scrollView.contentView.animator.bounds;
8951003
for (NSInteger i = 0; i < _tabs.count; i++) {
8961004
NSRect tabFrame = _tabs[i].frame;
8971005
if (!NSContainsRect(clipBounds, tabFrame)) {
898-
CGFloat allowance = i == _tabs.count - 1 ? 0 : NSWidth(tabFrame) * ScrollOneTabAllowance;
899-
if (NSMaxX(tabFrame) - allowance > NSMaxX(clipBounds)) {
1006+
const CGFloat allowance = (i == _tabs.count - 1) ?
1007+
0 : NSWidth(tabFrame) * ScrollOneTabAllowance;
1008+
const BOOL outOfBounds = [self useRightToLeft] ?
1009+
NSMinX(tabFrame) + allowance < NSMinX(clipBounds) :
1010+
NSMaxX(tabFrame) - allowance > NSMaxX(clipBounds);
1011+
if (outOfBounds) {
9001012
[self scrollTabToVisibleAtIndex:i];
9011013
break;
9021014
}

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)