Skip to content

Commit

Permalink
Merge pull request #1538 from ychin/mmtabs-rtl
Browse files Browse the repository at this point in the history
MMTabline: Add right-to-left (RTL) locale support
  • Loading branch information
ychin authored Jan 30, 2025
2 parents 62f5e1a + 3f8e405 commit a7db694
Show file tree
Hide file tree
Showing 4 changed files with 152 additions and 40 deletions.
4 changes: 2 additions & 2 deletions src/MacVim/MMAppController.m
Original file line number Diff line number Diff line change
Expand Up @@ -174,8 +174,8 @@ + (void)registerDefaults

NSDictionary *macvimDefaults = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithBool:NO], MMNoWindowKey,
[NSNumber numberWithInt:120], MMTabMinWidthKey,
[NSNumber numberWithInt:200], MMTabOptimumWidthKey,
[NSNumber numberWithInt:130], MMTabMinWidthKey,
[NSNumber numberWithInt:210], MMTabOptimumWidthKey,
[NSNumber numberWithBool:YES], MMShowAddTabButtonKey,
[NSNumber numberWithBool:NO], MMShowTabScrollButtonsKey,
[NSNumber numberWithInt:2], MMTextInsetLeftKey,
Expand Down
4 changes: 2 additions & 2 deletions src/MacVim/MMTabline/MMTabline.h
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@
- (void)selectTabAtIndex:(NSInteger)index;
- (MMTab *)tabAtIndex:(NSInteger)index;
- (void)scrollTabToVisibleAtIndex:(NSInteger)index;
- (void)scrollLeftOneTab;
- (void)scrollRightOneTab;
- (void)scrollBackwardOneTab;
- (void)scrollForwardOneTab;
- (void)setTablineSelBackground:(NSColor *)back foreground:(NSColor *)fore;

@end
Expand Down
180 changes: 146 additions & 34 deletions src/MacVim/MMTabline/MMTabline.m
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@
CGFloat remainder;
} TabWidth;

const CGFloat OptimumTabWidth = 220;
const CGFloat MinimumTabWidth = 100;
const CGFloat TabOverlap = 6;
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.
static const CGFloat OptimumTabWidth = 200;
static const CGFloat MinimumTabWidth = 100;
static const CGFloat TabOverlap = 6;
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.

static MMHoverButton* MakeHoverButton(MMTabline *tabline, MMHoverButtonImage imageType, NSString *tooltip, SEL action, BOOL continuous) {
MMHoverButton *button = [MMHoverButton new];
Expand Down Expand Up @@ -44,8 +44,8 @@ @implementation MMTabline
CGFloat _xOffsetForDrag;
NSInteger _initialDraggedTabIndex;
NSInteger _finalDraggedTabIndex;
MMHoverButton *_leftScrollButton;
MMHoverButton *_rightScrollButton;
MMHoverButton *_backwardScrollButton;
MMHoverButton *_forwardScrollButton;
id _scrollWheelEventMonitor;
}

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

_addTabButton = MakeHoverButton(self, MMHoverButtonImageAddTab, NSLocalizedString(@"create-new-tab-button", @"Create a new tab button"), @selector(addTabAtEnd), NO);
_leftScrollButton = MakeHoverButton(self, MMHoverButtonImageScrollLeft, NSLocalizedString(@"scroll-tabs-backward", @"Scroll backward button in tabs line"), @selector(scrollLeftOneTab), YES);
_rightScrollButton = MakeHoverButton(self, MMHoverButtonImageScrollRight, NSLocalizedString(@"scroll-tabs-forward", @"Scroll forward button in tabs line"), @selector(scrollRightOneTab), YES);

[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:[_leftScrollButton][_rightScrollButton]-5-[_scrollView]-5-[_addTabButton]" options:NSLayoutFormatAlignAllCenterY metrics:nil views:NSDictionaryOfVariableBindings(_scrollView, _leftScrollButton, _rightScrollButton, _addTabButton)]];
_addTabButton = MakeHoverButton(
self,
MMHoverButtonImageAddTab,
NSLocalizedString(@"create-new-tab-button", @"Create a new tab button"),
@selector(addTabAtEnd),
NO);
_backwardScrollButton = MakeHoverButton(
self,
[self useRightToLeft] ? MMHoverButtonImageScrollRight : MMHoverButtonImageScrollLeft,
NSLocalizedString(@"scroll-tabs-backward", @"Scroll backward button in tabs line"),
@selector(scrollBackwardOneTab),
YES);
_forwardScrollButton = MakeHoverButton(
self,
[self useRightToLeft] ? MMHoverButtonImageScrollLeft : MMHoverButtonImageScrollRight,
NSLocalizedString(@"scroll-tabs-forward", @"Scroll forward button in tabs line"),
@selector(scrollForwardOneTab),
YES);

[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:[_backwardScrollButton][_forwardScrollButton]-5-[_scrollView]-5-[_addTabButton]" options:NSLayoutFormatAlignAllCenterY metrics:nil views:NSDictionaryOfVariableBindings(_scrollView, _backwardScrollButton, _forwardScrollButton, _addTabButton)]];
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[_scrollView]|" options:0 metrics:nil views:@{@"_scrollView":_scrollView}]];

_tabScrollButtonsLeadingConstraint = [NSLayoutConstraint constraintWithItem:_leftScrollButton attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeLeading multiplier:1 constant:5];
_tabScrollButtonsLeadingConstraint = [NSLayoutConstraint constraintWithItem:_backwardScrollButton attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeLeading multiplier:1 constant:5];
[self addConstraint:_tabScrollButtonsLeadingConstraint];

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

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

[self addScrollWheelMonitor];

}
return self;
}
Expand Down Expand Up @@ -194,7 +211,7 @@ - (void)setShowsTabScrollButtons:(BOOL)showsTabScrollButtons
// (see -drawRect: in MMTab.m).
if (_showsTabScrollButtons != showsTabScrollButtons) {
_showsTabScrollButtons = showsTabScrollButtons;
_tabScrollButtonsLeadingConstraint.constant = showsTabScrollButtons ? 5 : -((NSWidth(_leftScrollButton.frame) * 2) + 5 + MMTabShadowBlurRadius);
_tabScrollButtonsLeadingConstraint.constant = showsTabScrollButtons ? 5 : -((NSWidth(_backwardScrollButton.frame) * 2) + 5 + MMTabShadowBlurRadius);
}
}

Expand Down Expand Up @@ -244,8 +261,8 @@ - (void)setTablineSelFgColor:(NSColor *)color
{
_tablineSelFgColor = color;
_addTabButton.fgColor = color;
_leftScrollButton.fgColor = color;
_rightScrollButton.fgColor = color;
_backwardScrollButton.fgColor = color;
_forwardScrollButton.fgColor = color;
for (MMTab *tab in _tabs) tab.state = tab.state;
}

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

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

#pragma mark - Helpers

NSComparisonResult SortTabsForZOrder(MMTab *tab1, MMTab *tab2, void *draggedTab)
NSComparisonResult SortTabsForZOrder(MMTab *tab1, MMTab *tab2, void *draggedTab, BOOL rtl)
{ // Z-order, highest to lowest: dragged, selected, hovered, rightmost
if (tab1 == (__bridge MMTab *)draggedTab) return NSOrderedDescending;
if (tab2 == (__bridge MMTab *)draggedTab) return NSOrderedAscending;
if (tab1.state == MMTabStateSelected) return NSOrderedDescending;
if (tab2.state == MMTabStateSelected) return NSOrderedAscending;
if (tab1.state == MMTabStateUnselectedHover) return NSOrderedDescending;
if (tab2.state == MMTabStateUnselectedHover) return NSOrderedAscending;
if (NSMinX(tab1.frame) < NSMinX(tab2.frame)) return NSOrderedAscending;
if (NSMinX(tab1.frame) > NSMinX(tab2.frame)) return NSOrderedDescending;
if (rtl) {
if (NSMinX(tab1.frame) > NSMinX(tab2.frame)) return NSOrderedAscending;
if (NSMinX(tab1.frame) < NSMinX(tab2.frame)) return NSOrderedDescending;
} else {
if (NSMinX(tab1.frame) < NSMinX(tab2.frame)) return NSOrderedAscending;
if (NSMinX(tab1.frame) > NSMinX(tab2.frame)) return NSOrderedDescending;
}
return NSOrderedSame;
}

NSComparisonResult SortTabsForZOrderLTR(MMTab *tab1, MMTab *tab2, void *draggedTab)
{
return SortTabsForZOrder(tab1, tab2, draggedTab, NO);
}


NSComparisonResult SortTabsForZOrderRTL(MMTab *tab1, MMTab *tab2, void *draggedTab)
{
return SortTabsForZOrder(tab1, tab2, draggedTab, YES);
}

- (TabWidth)tabWidthForTabs:(NSInteger)numTabs
{
// Each tab (except the first) overlaps the previous tab by TabOverlap
Expand Down Expand Up @@ -620,9 +655,17 @@ - (void)fixupCloseButtons

- (void)fixupTabZOrder
{
[_tabsContainer sortSubviewsUsingFunction:SortTabsForZOrder context:(__bridge void *)(_draggedTab)];
if ([self useRightToLeft]) {
[_tabsContainer sortSubviewsUsingFunction:SortTabsForZOrderRTL
context:(__bridge void *)(_draggedTab)];
} else {
[_tabsContainer sortSubviewsUsingFunction:SortTabsForZOrderLTR
context:(__bridge void *)(_draggedTab)];
}
}

/// The main layout function that calculates the tab positions and animate them
/// accordingly. Call this every time tabs have been added/removed/moved.
- (void)fixupLayoutWithAnimation:(BOOL)shouldAnimate delayResize:(BOOL)delayResize
{
if (!self.useAnimation)
Expand Down Expand Up @@ -656,6 +699,7 @@ - (void)fixupLayoutWithAnimation:(BOOL)shouldAnimate delayResize:(BOOL)delayResi
frame.size.width = i == _tabs.count - 1 ? t.width + t.remainder : t.width;
frame.origin.x = i != 0 ? i * (t.width - TabOverlap) : 0;
}
frame = [self flipRectRTL:frame];
if (shouldAnimate) {
[NSAnimationContext runAnimationGroup:^(NSAnimationContext * _Nonnull context) {
context.allowsImplicitAnimation = YES;
Expand All @@ -673,8 +717,20 @@ - (void)fixupLayoutWithAnimation:(BOOL)shouldAnimate delayResize:(BOOL)delayResi
NSRect frame = _tabsContainer.frame;
frame.size.width = t.width * _tabs.count - TabOverlap * (_tabs.count - 1);
frame.size.width = NSWidth(frame) < NSWidth(_scrollView.frame) ? NSWidth(_scrollView.frame) : NSWidth(frame);
if (shouldAnimate) _tabsContainer.animator.frame = frame;
else _tabsContainer.frame = frame;
const BOOL sizeDecreasing = NSWidth(frame) < NSWidth(_tabsContainer.frame);
if ([self useRightToLeft]) {
// In RTL mode we flip the X coords and grow from 0 to negative.
// See updateTabsContainerBoundsForRTL which auto-updates the
// bounds to match the frame.
frame.origin.x = -NSWidth(frame);
}
if (shouldAnimate && sizeDecreasing) {
// Need to animate to make sure we don't immediately get clamped by
// the new size if we are already scrolled all the way to the back.
_tabsContainer.animator.frame = frame;
} else {
_tabsContainer.frame = frame;
}
[self updateTabScrollButtonsEnabledState];
}
}
Expand All @@ -684,6 +740,41 @@ - (void)fixupLayoutWithAnimation:(BOOL)shouldAnimate
[self fixupLayoutWithAnimation:shouldAnimate delayResize:NO];
}

#pragma mark - Right-to-left (RTL) support

- (BOOL)useRightToLeft
{
// MMTabs support RTL locales. In such locales user interface items are
// laid out from right to left. The layout of hover buttons and views are
// automatically flipped by AppKit, but we need to handle this manually in
// the tab placement logic since that is custom logic.
return self.userInterfaceLayoutDirection == NSUserInterfaceLayoutDirectionRightToLeft;
}

- (void)updateTabsContainerBoundsForRTL:(NSNotification *)notification
{
// In RTL mode, we grow the tabs container to the left. We want to preserve
// stability of the scroll view's bounds, and also have the tabs animate
// correctly. To do this, we have to make sure the container bounds matches
// the frame at all times. This "cancels out" the negative X offsets with
// each other and ease calculations.
// E.g. an MMTab with origin (-100,0) inside the _tabsContainer coordinate
// space will actually be (-100,0) in the scroll view as well.
// In LTR mode we don't need this, since _tabsContainer's origin is always
// at (0,0).
_tabsContainer.bounds = _tabsContainer.frame;
}

- (NSRect)flipRectRTL:(NSRect)frame
{
if ([self useRightToLeft]) {
// In right-to-left mode, we flip the X coordinates for all the tabs so
// they start at 0 and grow in the negative direction.
frame.origin.x = -NSMaxX(frame);
}
return frame;
}

#pragma mark - Mouse

- (void)updateTrackingAreas
Expand Down Expand Up @@ -796,9 +887,15 @@ - (void)mouseDragged:(NSEvent *)event
[self fixupTabZOrder];
[_draggedTab setFrameOrigin:NSMakePoint(mouse.x - _xOffsetForDrag, 0)];
MMTab *selectedTab = _selectedTabIndex == -1 ? nil : _tabs[_selectedTabIndex];
const BOOL rightToLeft = [self useRightToLeft];
[_tabs sortWithOptions:NSSortStable usingComparator:^NSComparisonResult(MMTab *t1, MMTab *t2) {
if (NSMinX(t1.frame) <= NSMinX(t2.frame)) return NSOrderedAscending;
if (NSMinX(t1.frame) > NSMinX(t2.frame)) return NSOrderedDescending;
if (rightToLeft) {
if (NSMaxX(t1.frame) >= NSMaxX(t2.frame)) return NSOrderedAscending;
if (NSMaxX(t1.frame) < NSMaxX(t2.frame)) return NSOrderedDescending;
} else {
if (NSMinX(t1.frame) <= NSMinX(t2.frame)) return NSOrderedAscending;
if (NSMinX(t1.frame) > NSMinX(t2.frame)) return NSOrderedDescending;
}
return NSOrderedSame;
}];
_selectedTabIndex = _selectedTabIndex == -1 ? -1 : [_tabs indexOfObject:selectedTab];
Expand All @@ -820,11 +917,18 @@ - (void)updateTabScrollButtonsEnabledState
// on either side of _scrollView.
NSRect clipBounds = _scrollView.contentView.bounds;
if (NSWidth(_tabsContainer.frame) <= NSWidth(clipBounds)) {
_leftScrollButton.enabled = NO;
_rightScrollButton.enabled = NO;
_backwardScrollButton.enabled = NO;
_forwardScrollButton.enabled = NO;
} else {
_leftScrollButton.enabled = clipBounds.origin.x > 0;
_rightScrollButton.enabled = clipBounds.origin.x + NSWidth(clipBounds) < NSMaxX(_tabsContainer.frame);
BOOL scrollLeftEnabled = NSMinX(clipBounds) > NSMinX(_tabsContainer.frame);
BOOL scrollRightEnabled = NSMaxX(clipBounds) < NSMaxX(_tabsContainer.frame);
if ([self useRightToLeft]) {
_backwardScrollButton.enabled = scrollRightEnabled;
_forwardScrollButton.enabled = scrollLeftEnabled;
} else {
_backwardScrollButton.enabled = scrollLeftEnabled;
_forwardScrollButton.enabled = scrollRightEnabled;
}
}
}

Expand Down Expand Up @@ -874,29 +978,37 @@ - (void)scrollTabToVisibleAtIndex:(NSInteger)index
}
}

- (void)scrollLeftOneTab
- (void)scrollBackwardOneTab
{
NSRect clipBounds = _scrollView.contentView.animator.bounds;
for (NSInteger i = _tabs.count - 1; i >= 0; i--) {
NSRect tabFrame = _tabs[i].frame;
if (!NSContainsRect(clipBounds, tabFrame)) {
CGFloat allowance = i == 0 ? 0 : NSWidth(tabFrame) * ScrollOneTabAllowance;
if (NSMinX(tabFrame) + allowance < NSMinX(clipBounds)) {
const CGFloat allowance = (i == 0) ?
0 : NSWidth(tabFrame) * ScrollOneTabAllowance;
const BOOL outOfBounds = [self useRightToLeft] ?
NSMaxX(tabFrame) - allowance > NSMaxX(clipBounds) :
NSMinX(tabFrame) + allowance < NSMinX(clipBounds);
if (outOfBounds) {
[self scrollTabToVisibleAtIndex:i];
break;
}
}
}
}

- (void)scrollRightOneTab
- (void)scrollForwardOneTab
{
NSRect clipBounds = _scrollView.contentView.animator.bounds;
for (NSInteger i = 0; i < _tabs.count; i++) {
NSRect tabFrame = _tabs[i].frame;
if (!NSContainsRect(clipBounds, tabFrame)) {
CGFloat allowance = i == _tabs.count - 1 ? 0 : NSWidth(tabFrame) * ScrollOneTabAllowance;
if (NSMaxX(tabFrame) - allowance > NSMaxX(clipBounds)) {
const CGFloat allowance = (i == _tabs.count - 1) ?
0 : NSWidth(tabFrame) * ScrollOneTabAllowance;
const BOOL outOfBounds = [self useRightToLeft] ?
NSMinX(tabFrame) + allowance < NSMinX(clipBounds) :
NSMaxX(tabFrame) - allowance > NSMaxX(clipBounds);
if (outOfBounds) {
[self scrollTabToVisibleAtIndex:i];
break;
}
Expand Down
4 changes: 2 additions & 2 deletions src/MacVim/MMVimView.m
Original file line number Diff line number Diff line change
Expand Up @@ -264,12 +264,12 @@ - (IBAction)scrollToCurrentTab:(id)sender

- (IBAction)scrollBackwardOneTab:(id)sender
{
[tabline scrollLeftOneTab];
[tabline scrollBackwardOneTab];
}

- (IBAction)scrollForwardOneTab:(id)sender
{
[tabline scrollRightOneTab];
[tabline scrollForwardOneTab];
}

- (void)showTabline:(BOOL)on
Expand Down

0 comments on commit a7db694

Please sign in to comment.