Skip to content

Commit 2c52868

Browse files
authored
Merge pull request microsoft#203681 from microsoft/benibenj/ytterbic-rooster
Added Tab Drag and Drop Indicator
2 parents c93c6b2 + f664e0c commit 2c52868

File tree

4 files changed

+154
-89
lines changed

4 files changed

+154
-89
lines changed

build/lib/stylelint/vscode-known-variables.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -635,6 +635,7 @@
635635
"--vscode-tab-activeForeground",
636636
"--vscode-tab-activeModifiedBorder",
637637
"--vscode-tab-border",
638+
"--vscode-tab-dragAndDropBorder",
638639
"--vscode-tab-hoverBackground",
639640
"--vscode-tab-hoverBorder",
640641
"--vscode-tab-hoverForeground",

src/vs/workbench/browser/parts/editor/media/multieditortabscontrol.css

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
2929
[z-index] [kind]
3030
12 drag and drop overlay
31-
11 scrollbar
31+
11 scrollbar / tabs dnd border
3232
10 active-tab border-bottom
3333
9 tabs, title border bottom
3434
8 sticky-tab
@@ -220,14 +220,6 @@
220220
padding-right: 5px; /* we need less room when sizing is shrink/fixed */
221221
}
222222

223-
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.dragged {
224-
transform: translate3d(0px, 0px, 0px); /* forces tab to be drawn on a separate layer (fixes https://github.com/microsoft/vscode/issues/18733) */
225-
}
226-
227-
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.dragged-over div {
228-
pointer-events: none; /* prevents cursor flickering (fixes https://github.com/microsoft/vscode/issues/38753) */
229-
}
230-
231223
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.tab-actions-left:not(.sticky-compact) {
232224
flex-direction: row-reverse;
233225
padding-left: 0;
@@ -466,3 +458,38 @@
466458
/* When multiple tab bars are visible, only show editor actions for the last tab bar */
467459
display: none;
468460
}
461+
462+
/* Drag and drop target */
463+
464+
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.drop-target-left::after,
465+
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.drop-target-right::before {
466+
content: "";
467+
position: absolute;
468+
top: 0;
469+
height: 100%;
470+
width: 1px;
471+
background-color: var(--vscode-tab-dragAndDropBorder);
472+
pointer-events: none;
473+
z-index: 11;
474+
}
475+
476+
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.drop-target-right::before {
477+
left: 0;
478+
}
479+
480+
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.drop-target-left::after {
481+
right: -1px; /* -1 to connect with drop-target-right */
482+
}
483+
484+
/* Make drop target edge cases more visible (wrapped tabs & first/last) */
485+
486+
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.last-in-row.drop-target-left:not(:last-child)::after {
487+
right: 0px;
488+
}
489+
490+
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.last-in-row.drop-target-left::after,
491+
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.last-in-row + .tab.drop-target-right::before,
492+
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab:last-child.drop-target-left::after,
493+
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab:first-child.drop-target-right::before {
494+
width: 2px;
495+
}

src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts

Lines changed: 106 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,6 @@ export class MultiEditorTabsControl extends EditorTabsControl {
335335

336336
// Return if the target is not on the tabs container
337337
if (e.target !== tabsContainer) {
338-
this.updateDropFeedback(tabsContainer, false); // fixes https://github.com/microsoft/vscode/issues/52093
339338
return;
340339
}
341340

@@ -348,49 +347,31 @@ export class MultiEditorTabsControl extends EditorTabsControl {
348347
return;
349348
}
350349

351-
// Return if dragged editor is last tab because then this is a no-op
352-
let isLocalDragAndDrop = false;
353-
if (this.editorTransfer.hasData(DraggedEditorIdentifier.prototype)) {
354-
isLocalDragAndDrop = true;
355-
356-
const data = this.editorTransfer.getData(DraggedEditorIdentifier.prototype);
357-
if (Array.isArray(data)) {
358-
const localDraggedEditor = data[0].identifier;
359-
if (this.groupView.id === localDraggedEditor.groupId && this.tabsModel.isLast(localDraggedEditor.editor)) {
360-
if (e.dataTransfer) {
361-
e.dataTransfer.dropEffect = 'none';
362-
}
363-
364-
return;
365-
}
366-
}
367-
}
368-
369350
// Update the dropEffect to "copy" if there is no local data to be dragged because
370351
// in that case we can only copy the data into and not move it from its source
371-
if (!isLocalDragAndDrop) {
352+
if (!this.editorTransfer.hasData(DraggedEditorIdentifier.prototype)) {
372353
if (e.dataTransfer) {
373354
e.dataTransfer.dropEffect = 'copy';
374355
}
375356
}
376357

377-
this.updateDropFeedback(tabsContainer, true);
358+
this.updateDropFeedback(tabsContainer, true, e);
378359
},
379360

380361
onDragLeave: e => {
381-
this.updateDropFeedback(tabsContainer, false);
362+
this.updateDropFeedback(tabsContainer, false, e);
382363
tabsContainer.classList.remove('scroll');
383364
},
384365

385366
onDragEnd: e => {
386-
this.updateDropFeedback(tabsContainer, false);
367+
this.updateDropFeedback(tabsContainer, false, e);
387368
tabsContainer.classList.remove('scroll');
388369

389370
this.onGroupDragEnd(e, lastDragEvent, tabsContainer, isNewWindowOperation);
390371
},
391372

392373
onDrop: e => {
393-
this.updateDropFeedback(tabsContainer, false);
374+
this.updateDropFeedback(tabsContainer, false, e);
394375
tabsContainer.classList.remove('scroll');
395376

396377
if (e.target === tabsContainer) {
@@ -1038,14 +1019,13 @@ export class MultiEditorTabsControl extends EditorTabsControl {
10381019

10391020
if (e.dataTransfer) {
10401021
e.dataTransfer.effectAllowed = 'copyMove';
1022+
e.dataTransfer.setDragImage(tab, 0, 0); // top left corner of dragged tab set to cursor position to make room for drop-border feedback
10411023
}
10421024

10431025
// Apply some datatransfer types to allow for dragging the element outside of the application
10441026
this.doFillResourceDataTransfers([editor], e, isNewWindowOperation);
10451027

1046-
// Fixes https://github.com/microsoft/vscode/issues/18733
1047-
tab.classList.add('dragged');
1048-
scheduleAtNextAnimationFrame(getWindow(this.parent), () => tab.classList.remove('dragged'));
1028+
scheduleAtNextAnimationFrame(getWindow(this.parent), () => this.updateDropFeedback(tab, false, e, tabIndex));
10491029
},
10501030

10511031
onDrag: e => {
@@ -1054,9 +1034,6 @@ export class MultiEditorTabsControl extends EditorTabsControl {
10541034

10551035
onDragEnter: e => {
10561036

1057-
// Update class to signal drag operation
1058-
tab.classList.add('dragged-over');
1059-
10601037
// Return if transfer is unsupported
10611038
if (!this.isSupportedDropTransfer(e)) {
10621039
if (e.dataTransfer) {
@@ -1066,52 +1043,30 @@ export class MultiEditorTabsControl extends EditorTabsControl {
10661043
return;
10671044
}
10681045

1069-
// Return if dragged editor is the current tab dragged over
1070-
let isLocalDragAndDrop = false;
1071-
if (this.editorTransfer.hasData(DraggedEditorIdentifier.prototype)) {
1072-
isLocalDragAndDrop = true;
1073-
1074-
const data = this.editorTransfer.getData(DraggedEditorIdentifier.prototype);
1075-
if (Array.isArray(data)) {
1076-
const localDraggedEditor = data[0].identifier;
1077-
if (localDraggedEditor.editor === this.tabsModel.getEditorByIndex(tabIndex) && localDraggedEditor.groupId === this.groupView.id) {
1078-
if (e.dataTransfer) {
1079-
e.dataTransfer.dropEffect = 'none';
1080-
}
1081-
1082-
return;
1083-
}
1084-
}
1085-
}
1086-
10871046
// Update the dropEffect to "copy" if there is no local data to be dragged because
10881047
// in that case we can only copy the data into and not move it from its source
1089-
if (!isLocalDragAndDrop) {
1048+
if (!this.editorTransfer.hasData(DraggedEditorIdentifier.prototype)) {
10901049
if (e.dataTransfer) {
10911050
e.dataTransfer.dropEffect = 'copy';
10921051
}
10931052
}
10941053

1095-
this.updateDropFeedback(tab, true, tabIndex);
1054+
this.updateDropFeedback(tab, true, e, tabIndex);
10961055
},
10971056

1098-
onDragOver: (_, dragDuration) => {
1057+
onDragOver: (e, dragDuration) => {
10991058
if (dragDuration >= MultiEditorTabsControl.DRAG_OVER_OPEN_TAB_THRESHOLD) {
11001059
const draggedOverTab = this.tabsModel.getEditorByIndex(tabIndex);
11011060
if (draggedOverTab && this.tabsModel.activeEditor !== draggedOverTab) {
11021061
this.groupView.openEditor(draggedOverTab, { preserveFocus: true });
11031062
}
11041063
}
1105-
},
11061064

1107-
onDragLeave: () => {
1108-
tab.classList.remove('dragged-over');
1109-
this.updateDropFeedback(tab, false, tabIndex);
1065+
this.updateDropFeedback(tab, true, e, tabIndex);
11101066
},
11111067

11121068
onDragEnd: async e => {
1113-
tab.classList.remove('dragged-over');
1114-
this.updateDropFeedback(tab, false, tabIndex);
1069+
this.updateDropFeedback(tab, false, e, tabIndex);
11151070

11161071
this.editorTransfer.clearData(DraggedEditorIdentifier.prototype);
11171072

@@ -1140,10 +1095,31 @@ export class MultiEditorTabsControl extends EditorTabsControl {
11401095
},
11411096

11421097
onDrop: e => {
1143-
tab.classList.remove('dragged-over');
1144-
this.updateDropFeedback(tab, false, tabIndex);
1098+
this.updateDropFeedback(tab, false, e, tabIndex);
1099+
1100+
// compute the target index
1101+
let targetIndex = tabIndex;
1102+
if (this.getTabDragOverLocation(e, tab) === 'right') {
1103+
targetIndex++;
1104+
}
1105+
1106+
// If we are moving an editor inside the same group and it is
1107+
// located before the target index we need to reduce the index
1108+
// by one to account for the fact that the move will cause all
1109+
// subsequent tabs to move one to the left.
1110+
const editorIdentifiers = this.editorTransfer.getData(DraggedEditorIdentifier.prototype);
1111+
if (editorIdentifiers !== undefined) {
1112+
const draggedEditorIdentifier = editorIdentifiers[0].identifier;
1113+
const sourceGroup = this.editorPartsView.getGroup(draggedEditorIdentifier.groupId);
1114+
if (sourceGroup?.id === this.groupView.id) {
1115+
const editorIndex = sourceGroup.getIndexOfEditor(draggedEditorIdentifier.editor);
1116+
if (editorIndex < targetIndex) {
1117+
targetIndex--;
1118+
}
1119+
}
1120+
}
11451121

1146-
this.onDrop(e, tabIndex, tabsContainer);
1122+
this.onDrop(e, targetIndex, tabsContainer);
11471123
}
11481124
}));
11491125

@@ -1174,28 +1150,73 @@ export class MultiEditorTabsControl extends EditorTabsControl {
11741150
return false;
11751151
}
11761152

1177-
private updateDropFeedback(element: HTMLElement, isDND: boolean, tabIndex?: number): void {
1153+
private updateDropFeedback(element: HTMLElement, isDND: boolean, e: DragEvent, tabIndex?: number): void {
11781154
const isTab = (typeof tabIndex === 'number');
1179-
const editor = typeof tabIndex === 'number' ? this.tabsModel.getEditorByIndex(tabIndex) : undefined;
1180-
const isActiveTab = isTab && !!editor && this.tabsModel.isActive(editor);
1181-
1182-
// Background
1183-
const noDNDBackgroundColor = isTab ? this.getColor(isActiveTab ? TAB_ACTIVE_BACKGROUND : TAB_INACTIVE_BACKGROUND) : '';
1184-
element.style.backgroundColor = (isDND ? this.getColor(EDITOR_DRAG_AND_DROP_BACKGROUND) : noDNDBackgroundColor) || '';
1185-
1186-
// Outline
1187-
const activeContrastBorderColor = this.getColor(activeContrastBorder);
1188-
if (activeContrastBorderColor && isDND) {
1189-
element.style.outlineWidth = '2px';
1190-
element.style.outlineStyle = 'dashed';
1191-
element.style.outlineColor = activeContrastBorderColor;
1192-
element.style.outlineOffset = isTab ? '-5px' : '-3px';
1155+
1156+
let dropTarget;
1157+
if (isDND) {
1158+
if (isTab) {
1159+
dropTarget = this.computeDropTarget(e, tabIndex, element);
1160+
} else {
1161+
dropTarget = { leftElement: element.lastElementChild as HTMLElement, rightElement: undefined };
1162+
}
11931163
} else {
1194-
element.style.outlineWidth = '';
1195-
element.style.outlineStyle = '';
1196-
element.style.outlineColor = activeContrastBorderColor || '';
1197-
element.style.outlineOffset = '';
1164+
dropTarget = undefined;
11981165
}
1166+
1167+
this.updateDropTarget(dropTarget);
1168+
}
1169+
1170+
private dropTarget: { leftElement: HTMLElement | undefined; rightElement: HTMLElement | undefined } | undefined;
1171+
private updateDropTarget(newTarget: { leftElement: HTMLElement | undefined; rightElement: HTMLElement | undefined } | undefined): void {
1172+
const oldTargets = this.dropTarget;
1173+
if (oldTargets === newTarget || oldTargets && newTarget && oldTargets.leftElement === newTarget.leftElement && oldTargets.rightElement === newTarget.rightElement) {
1174+
return;
1175+
}
1176+
1177+
const dropClassLeft = 'drop-target-left';
1178+
const dropClassRight = 'drop-target-right';
1179+
1180+
if (oldTargets) {
1181+
oldTargets.leftElement?.classList.remove(dropClassLeft);
1182+
oldTargets.rightElement?.classList.remove(dropClassRight);
1183+
}
1184+
1185+
if (newTarget) {
1186+
newTarget.leftElement?.classList.add(dropClassLeft);
1187+
newTarget.rightElement?.classList.add(dropClassRight);
1188+
}
1189+
1190+
this.dropTarget = newTarget;
1191+
}
1192+
1193+
private getTabDragOverLocation(e: DragEvent, tab: HTMLElement): 'left' | 'right' {
1194+
const rect = tab.getBoundingClientRect();
1195+
const offsetXRelativeToParent = e.clientX - rect.left;
1196+
1197+
return offsetXRelativeToParent <= rect.width / 2 ? 'left' : 'right';
1198+
}
1199+
1200+
private computeDropTarget(e: DragEvent, tabIndex: number, targetTab: HTMLElement): { leftElement: HTMLElement | undefined; rightElement: HTMLElement | undefined } | undefined {
1201+
const isLeftSideOfTab = this.getTabDragOverLocation(e, targetTab) === 'left';
1202+
const isLastTab = tabIndex === this.tabsModel.count - 1;
1203+
const isFirstTab = tabIndex === 0;
1204+
1205+
// Before first tab
1206+
if (isLeftSideOfTab && isFirstTab) {
1207+
return { leftElement: undefined, rightElement: targetTab };
1208+
}
1209+
1210+
// After last tab
1211+
if (!isLeftSideOfTab && isLastTab) {
1212+
return { leftElement: targetTab, rightElement: undefined };
1213+
}
1214+
1215+
// Between two tabs
1216+
const tabBefore = isLeftSideOfTab ? targetTab.previousElementSibling : targetTab;
1217+
const tabAfter = isLeftSideOfTab ? targetTab : targetTab.nextElementSibling;
1218+
1219+
return { leftElement: tabBefore as HTMLElement, rightElement: tabAfter as HTMLElement };
11991220
}
12001221

12011222
private computeTabLabels(): void {
@@ -1729,6 +1750,11 @@ export class MultiEditorTabsControl extends EditorTabsControl {
17291750
// positioned editor actions container when tabs wrap. The margin needs to
17301751
// be the width of the editor actions container to avoid screen cheese.
17311752
tabsContainer.style.setProperty('--last-tab-margin-right', tabsWrapMultiLine ? `${editorToolbarContainer.offsetWidth}px` : '0');
1753+
1754+
// Remove old css classes that are not needed anymore
1755+
for (const tab of tabsContainer.children) {
1756+
tab.classList.remove('last-in-row');
1757+
}
17321758
}
17331759

17341760
// Setting enabled: selectively enable wrapping if possible
@@ -2044,7 +2070,7 @@ export class MultiEditorTabsControl extends EditorTabsControl {
20442070
private async onDrop(e: DragEvent, targetTabIndex: number, tabsContainer: HTMLElement): Promise<void> {
20452071
EventHelper.stop(e, true);
20462072

2047-
this.updateDropFeedback(tabsContainer, false);
2073+
this.updateDropFeedback(tabsContainer, false, e, targetTabIndex);
20482074
tabsContainer.classList.remove('scroll');
20492075

20502076
const targetEditorIndex = this.tabsModel instanceof UnstickyEditorGroupModel ? targetTabIndex + this.groupView.stickyCount : targetTabIndex;

src/vs/workbench/common/theme.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,17 @@ export const TAB_UNFOCUSED_HOVER_BORDER = registerColor('tab.unfocusedHoverBorde
182182

183183
//#endregion
184184

185+
//#region Tab Drag and Drop Border
186+
187+
export const TAB_DRAG_AND_DROP_BORDER = registerColor('tab.dragAndDropBorder', {
188+
dark: TAB_ACTIVE_FOREGROUND,
189+
light: TAB_ACTIVE_FOREGROUND,
190+
hcDark: activeContrastBorder,
191+
hcLight: activeContrastBorder
192+
}, localize('tabDragAndDropBorder', "Border between tabs to indicate that a tab can be inserted between two tabs. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups."));
193+
194+
//#endregion
195+
185196
//#region Tab Modified Border
186197

187198
export const TAB_ACTIVE_MODIFIED_BORDER = registerColor('tab.activeModifiedBorder', {

0 commit comments

Comments
 (0)