Skip to content

Commit

Permalink
Make GUI tabs track Vim tabs in updates and animate correctly
Browse files Browse the repository at this point in the history
MMTabline was introduced in #1120, which replaced the ancient
PSMTabBarControl for representing Vim tabs. It uses animation to handle
tab layouts, but it only worked in some situations, due to the Vim IPC
API only sending a tabline update with all the tab labels with no way to
track individual tabs. Update the API so that Vim now sends individual
unique IDs in the update message as well to allow the GUI to track tabs
over time and animate them. Vim does not interally have a concept of
unique tab IDs, but we can use the pointer to the structure as such
because they are allocated as a linked list and will never change.

Extend MMTabline to have a new `updateTabsByTags` API to batch update
all tabs in one go, which will diff the new tags with existing tabs and
create/remove/move tabs as necessary. The scrolling logic has also been
moved inside it and it now only scrolls to selected tab when it has
changed or moved. When deleting tabs we won't scroll as usually the user
doesn't expect it. Dragging tabs also now work correctly when it is
removed during a drag, or another tab has been selected.

This does mean the add/close tab APIs are currently unused, as the only
entrypoint for modifying tabs from MacVim is currently only via
`updateTabsByTags`.

After this, different Vim operations will now animate correctly,
including `:tabmove`, `:bdelete` (which could remove multiple tabs at
once), `:tabnew`, `:tabclose`.
  • Loading branch information
ychin committed Jan 27, 2025
1 parent 8a68481 commit b589c84
Show file tree
Hide file tree
Showing 9 changed files with 294 additions and 149 deletions.
36 changes: 20 additions & 16 deletions src/MacVim/MMBackend.m
Original file line number Diff line number Diff line change
Expand Up @@ -764,32 +764,36 @@ - (void)selectTab:(int)index

- (void)updateTabBar
{
// Update the tab bar with the most up-to-date info, including number of
// tabs and titles/tooltips. MacVim would also like to know which specific
// tabs were moved/added/deleted in order for animation to work, but Vim
// does not have specific callbacks to listen to that. Instead, since the
// tabpage_T memory address is constant per tab, we use that as a permanent
// identifier for each GUI tab so MacVim can do the association.
NSMutableData *data = [NSMutableData data];

// 1. Current selected tab index
int idx = tabpage_index(curtab) - 1;
[data appendBytes:&idx length:sizeof(int)];

tabpage_T *tp;
// 2. Unique id for all the tabs
// Do these first so they appear as a consecutive memory block.
for (tp = first_tabpage; tp != NULL; tp = tp->tp_next) {
// Count the number of windows in the tabpage.
//win_T *wp = tp->tp_firstwin;
//int wincount;
//for (wincount = 0; wp != NULL; wp = wp->w_next, ++wincount);
//[data appendBytes:&wincount length:sizeof(int)];

int tabProp = MMTabInfoCount;
[data appendBytes:&tabProp length:sizeof(int)];
for (tabProp = MMTabLabel; tabProp < MMTabInfoCount; ++tabProp) {
[data appendBytes:&tp length:sizeof(void*)];
}
// Null terminate the unique IDs.
tp = 0;
[data appendBytes:&tp length:sizeof(void*)];
// 3. Labels and tooltips of each tab
for (tp = first_tabpage; tp != NULL; tp = tp->tp_next) {
for (int tabProp = MMTabLabel; tabProp < MMTabInfoCount; ++tabProp) {
// This function puts the label of the tab in the global 'NameBuff'.
get_tabline_label(tp, (tabProp == MMTabToolTip));
NSString *s = [NSString stringWithVimString:NameBuff];
int len = [s lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
if (len < 0)
len = 0;

[data appendBytes:&len length:sizeof(int)];
size_t len = STRLEN(NameBuff);
[data appendBytes:&len length:sizeof(size_t)];
if (len > 0)
[data appendBytes:[s UTF8String] length:len];
[data appendBytes:NameBuff length:len];
}
}

Expand Down
1 change: 1 addition & 0 deletions src/MacVim/MMTabline/MMTab.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ typedef enum : NSInteger {

@interface MMTab : NSView

@property (nonatomic, readwrite) NSInteger tag; ///< Unique identifier that caller can set for the tab
@property (nonatomic, copy) NSString *title;
@property (nonatomic, getter=isCloseButtonHidden) BOOL closeButtonHidden;
@property (nonatomic) MMTabState state;
Expand Down
2 changes: 2 additions & 0 deletions src/MacVim/MMTabline/MMTab.m
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ @implementation MMTab
NSTextField *_titleLabel;
}

@synthesize tag = _tag;

+ (id)defaultAnimationForKey:(NSAnimatablePropertyKey)key
{
if ([key isEqualToString:@"fillColor"]) {
Expand Down
24 changes: 23 additions & 1 deletion src/MacVim/MMTabline/MMTabline.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@

@interface MMTabline : NSView

@property (nonatomic) NSInteger selectedTabIndex;
/// The index of the selected tab. Can be -1 if nothing is selected.
@property (nonatomic, readonly) NSInteger selectedTabIndex;
@property (nonatomic) NSInteger optimumTabWidth;
@property (nonatomic) NSInteger minimumTabWidth;
@property (nonatomic) BOOL showsAddTabButton;
Expand All @@ -24,10 +25,31 @@
@property (nonatomic, retain) NSColor *tablineFillFgColor;
@property (nonatomic, weak) id <MMTablineDelegate> delegate;

/// Add a tab at the end. It's not selected automatically.
- (NSInteger)addTabAtEnd;
/// Add a tab after the selected one. It's not selected automatically.
- (NSInteger)addTabAfterSelectedTab;
/// Add a tab at specified index. It's not selected automatically.
- (NSInteger)addTabAtIndex:(NSInteger)index;

- (void)closeTab:(MMTab *)tab force:(BOOL)force layoutImmediately:(BOOL)layoutImmediately;

/// Batch update all the tabs using tab tags as unique IDs. Tab line will handle
/// creating / removing tabs as necessary, and moving tabs to their new
/// positions.
///
/// The tags array has to have unique items only, and each existing MMTab also
/// has to have unique tags.
///
/// @param tags The list of unique tags that are cross-referenced with each
/// MMTab's tag. Order within the array indicates the desired tab order.
/// @param len The length of the tags array.
/// @param delayTabResize If true, do not resize tab widths until the the tab
/// line loses focus. This helps preserve the relative tab positions and
/// lines up the close buttons to the previous tab. This will also
/// prevent scrolling to the new selected tab.
- (void)updateTabsByTags:(NSInteger *)tags len:(NSUInteger)len delayTabResize:(BOOL)delayTabResize;

- (void)selectTabAtIndex:(NSInteger)index;
- (MMTab *)tabAtIndex:(NSInteger)index;
- (void)scrollTabToVisibleAtIndex:(NSInteger)index;
Expand Down
Loading

0 comments on commit b589c84

Please sign in to comment.