Skip to content

Commit c3f7a1c

Browse files
committed
Added tab drag and drop between tabs indicator
1 parent fa55876 commit c3f7a1c

File tree

3 files changed

+159
-63
lines changed

3 files changed

+159
-63
lines changed

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

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -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,41 @@
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+
pointer-events: none;
472+
}
473+
474+
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.drop-target-right::before {
475+
left: 0;
476+
}
477+
478+
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.drop-target-left::after {
479+
right: -1px; /* -1 to connect with drop-target-right */
480+
}
481+
482+
/* Make drop target edge cases more visible (wrapped tabs & first/last) */
483+
484+
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.last-in-row.drop-target-left:not(:last-child)::after {
485+
right: 1px;
486+
}
487+
488+
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.drop-target-right:first-child:before,
489+
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.last-in-row + .tab.drop-target-right::before {
490+
left: 1px;
491+
}
492+
493+
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.last-in-row.drop-target-left::after,
494+
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.last-in-row + .tab.drop-target-right::before,
495+
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab:last-child.drop-target-left::after,
496+
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab:first-child.drop-target-right::before {
497+
width: 2px;
498+
}

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

Lines changed: 110 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import { ScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElemen
2626
import { ScrollbarVisibility } from 'vs/base/common/scrollable';
2727
import { getOrSet } from 'vs/base/common/map';
2828
import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
29-
import { TAB_INACTIVE_BACKGROUND, TAB_ACTIVE_BACKGROUND, TAB_ACTIVE_FOREGROUND, TAB_INACTIVE_FOREGROUND, TAB_BORDER, EDITOR_DRAG_AND_DROP_BACKGROUND, TAB_UNFOCUSED_ACTIVE_FOREGROUND, TAB_UNFOCUSED_INACTIVE_FOREGROUND, TAB_UNFOCUSED_ACTIVE_BACKGROUND, TAB_UNFOCUSED_ACTIVE_BORDER, TAB_ACTIVE_BORDER, TAB_HOVER_BACKGROUND, TAB_HOVER_BORDER, TAB_UNFOCUSED_HOVER_BACKGROUND, TAB_UNFOCUSED_HOVER_BORDER, EDITOR_GROUP_HEADER_TABS_BACKGROUND, WORKBENCH_BACKGROUND, TAB_ACTIVE_BORDER_TOP, TAB_UNFOCUSED_ACTIVE_BORDER_TOP, TAB_ACTIVE_MODIFIED_BORDER, TAB_INACTIVE_MODIFIED_BORDER, TAB_UNFOCUSED_ACTIVE_MODIFIED_BORDER, TAB_UNFOCUSED_INACTIVE_MODIFIED_BORDER, TAB_UNFOCUSED_INACTIVE_BACKGROUND, TAB_HOVER_FOREGROUND, TAB_UNFOCUSED_HOVER_FOREGROUND, EDITOR_GROUP_HEADER_TABS_BORDER, TAB_LAST_PINNED_BORDER } from 'vs/workbench/common/theme';
29+
import { TAB_INACTIVE_BACKGROUND, TAB_ACTIVE_BACKGROUND, TAB_ACTIVE_FOREGROUND, TAB_INACTIVE_FOREGROUND, TAB_BORDER, EDITOR_DRAG_AND_DROP_BACKGROUND, TAB_UNFOCUSED_ACTIVE_FOREGROUND, TAB_UNFOCUSED_INACTIVE_FOREGROUND, TAB_UNFOCUSED_ACTIVE_BACKGROUND, TAB_UNFOCUSED_ACTIVE_BORDER, TAB_ACTIVE_BORDER, TAB_HOVER_BACKGROUND, TAB_HOVER_BORDER, TAB_UNFOCUSED_HOVER_BACKGROUND, TAB_UNFOCUSED_HOVER_BORDER, EDITOR_GROUP_HEADER_TABS_BACKGROUND, WORKBENCH_BACKGROUND, TAB_ACTIVE_BORDER_TOP, TAB_UNFOCUSED_ACTIVE_BORDER_TOP, TAB_ACTIVE_MODIFIED_BORDER, TAB_INACTIVE_MODIFIED_BORDER, TAB_UNFOCUSED_ACTIVE_MODIFIED_BORDER, TAB_UNFOCUSED_INACTIVE_MODIFIED_BORDER, TAB_UNFOCUSED_INACTIVE_BACKGROUND, TAB_HOVER_FOREGROUND, TAB_UNFOCUSED_HOVER_FOREGROUND, EDITOR_GROUP_HEADER_TABS_BORDER, TAB_LAST_PINNED_BORDER, TAB_Drag_And_Drop_Between_Indicator } from 'vs/workbench/common/theme';
3030
import { activeContrastBorder, contrastBorder, editorBackground } from 'vs/platform/theme/common/colorRegistry';
3131
import { ResourcesDropHandler, DraggedEditorIdentifier, DraggedEditorGroupIdentifier, extractTreeDropData, isWindowDraggedOver } from 'vs/workbench/browser/dnd';
3232
import { Color } from 'vs/base/common/color';
@@ -132,6 +132,29 @@ export class MultiEditorTabsControl extends EditorTabsControl {
132132
private lastMouseWheelEventTime = 0;
133133
private isMouseOverTabs = false;
134134

135+
private _dndDropTarget: { leftElement: HTMLElement | undefined; rightElement: HTMLElement | undefined } | undefined;
136+
private set dndDropTarget(target: { leftElement: HTMLElement | undefined; rightElement: HTMLElement | undefined } | undefined) {
137+
const oldTargets = this._dndDropTarget;
138+
if (oldTargets === target || oldTargets && target && oldTargets.leftElement === target.leftElement && oldTargets.rightElement === target.rightElement) {
139+
return;
140+
}
141+
142+
const dropClassLeft = 'drop-target-left';
143+
const dropClassRight = 'drop-target-right';
144+
145+
if (oldTargets) {
146+
oldTargets.leftElement?.classList.remove(dropClassLeft);
147+
oldTargets.rightElement?.classList.remove(dropClassRight);
148+
}
149+
150+
if (target) {
151+
target.leftElement?.classList.add(dropClassLeft);
152+
target.rightElement?.classList.add(dropClassRight);
153+
}
154+
155+
this._dndDropTarget = target;
156+
}
157+
135158
constructor(
136159
parent: HTMLElement,
137160
editorPartsView: IEditorPartsView,
@@ -335,7 +358,6 @@ export class MultiEditorTabsControl extends EditorTabsControl {
335358

336359
// Return if the target is not on the tabs container
337360
if (e.target !== tabsContainer) {
338-
this.updateDropFeedback(tabsContainer, false); // fixes https://github.com/microsoft/vscode/issues/52093
339361
return;
340362
}
341363

@@ -352,18 +374,6 @@ export class MultiEditorTabsControl extends EditorTabsControl {
352374
let isLocalDragAndDrop = false;
353375
if (this.editorTransfer.hasData(DraggedEditorIdentifier.prototype)) {
354376
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-
}
367377
}
368378

369379
// Update the dropEffect to "copy" if there is no local data to be dragged because
@@ -374,23 +384,23 @@ export class MultiEditorTabsControl extends EditorTabsControl {
374384
}
375385
}
376386

377-
this.updateDropFeedback(tabsContainer, true);
387+
this.updateDropFeedback(tabsContainer, true, e);
378388
},
379389

380390
onDragLeave: e => {
381-
this.updateDropFeedback(tabsContainer, false);
391+
this.updateDropFeedback(tabsContainer, false, e);
382392
tabsContainer.classList.remove('scroll');
383393
},
384394

385395
onDragEnd: e => {
386-
this.updateDropFeedback(tabsContainer, false);
396+
this.updateDropFeedback(tabsContainer, false, e);
387397
tabsContainer.classList.remove('scroll');
388398

389399
this.onGroupDragEnd(e, lastDragEvent, tabsContainer, isNewWindowOperation);
390400
},
391401

392402
onDrop: e => {
393-
this.updateDropFeedback(tabsContainer, false);
403+
this.updateDropFeedback(tabsContainer, false, e);
394404
tabsContainer.classList.remove('scroll');
395405

396406
if (e.target === tabsContainer) {
@@ -1038,25 +1048,22 @@ export class MultiEditorTabsControl extends EditorTabsControl {
10381048

10391049
if (e.dataTransfer) {
10401050
e.dataTransfer.effectAllowed = 'copyMove';
1051+
e.dataTransfer.setDragImage(tab, 0, 0); // top left corner of dragged tab set to cursor position
10411052
}
10421053

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

10461057
// Fixes https://github.com/microsoft/vscode/issues/18733
1047-
tab.classList.add('dragged');
1048-
scheduleAtNextAnimationFrame(getWindow(this.parent), () => tab.classList.remove('dragged'));
1058+
this.updateDropFeedback(tab.cloneNode(true) as HTMLElement, true, e, tabIndex);
1059+
scheduleAtNextAnimationFrame(getWindow(this.parent), () => this.updateDropFeedback(tab, false, e, tabIndex));
10491060
},
10501061

10511062
onDrag: e => {
10521063
lastDragEvent = e;
10531064
},
10541065

10551066
onDragEnter: e => {
1056-
1057-
// Update class to signal drag operation
1058-
tab.classList.add('dragged-over');
1059-
10601067
// Return if transfer is unsupported
10611068
if (!this.isSupportedDropTransfer(e)) {
10621069
if (e.dataTransfer) {
@@ -1066,22 +1073,9 @@ export class MultiEditorTabsControl extends EditorTabsControl {
10661073
return;
10671074
}
10681075

1069-
// Return if dragged editor is the current tab dragged over
10701076
let isLocalDragAndDrop = false;
10711077
if (this.editorTransfer.hasData(DraggedEditorIdentifier.prototype)) {
10721078
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-
}
10851079
}
10861080

10871081
// Update the dropEffect to "copy" if there is no local data to be dragged because
@@ -1092,26 +1086,24 @@ export class MultiEditorTabsControl extends EditorTabsControl {
10921086
}
10931087
}
10941088

1095-
this.updateDropFeedback(tab, true, tabIndex);
1089+
this.updateDropFeedback(tab, true, e, tabIndex);
10961090
},
10971091

1098-
onDragOver: (_, dragDuration) => {
1092+
onDragOver: (e, dragDuration) => {
10991093
if (dragDuration >= MultiEditorTabsControl.DRAG_OVER_OPEN_TAB_THRESHOLD) {
11001094
const draggedOverTab = this.tabsModel.getEditorByIndex(tabIndex);
11011095
if (draggedOverTab && this.tabsModel.activeEditor !== draggedOverTab) {
11021096
this.groupView.openEditor(draggedOverTab, { preserveFocus: true });
11031097
}
11041098
}
1105-
},
11061099

1107-
onDragLeave: () => {
1108-
tab.classList.remove('dragged-over');
1109-
this.updateDropFeedback(tab, false, tabIndex);
1100+
if (e.dataTransfer?.dropEffect !== 'none') {
1101+
this.updateDropFeedback(tab, true, e, tabIndex);
1102+
}
11101103
},
11111104

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

11161108
this.editorTransfer.clearData(DraggedEditorIdentifier.prototype);
11171109

@@ -1140,10 +1132,29 @@ export class MultiEditorTabsControl extends EditorTabsControl {
11401132
},
11411133

11421134
onDrop: e => {
1143-
tab.classList.remove('dragged-over');
1144-
this.updateDropFeedback(tab, false, tabIndex);
1135+
this.updateDropFeedback(tab, false, e, tabIndex);
1136+
1137+
// compute the target index
1138+
let targetIndex = tabIndex;
1139+
if (!this.isHeadOfTab(e, tab)) {
1140+
targetIndex++;
1141+
}
1142+
1143+
const editorIdentifiers = this.editorTransfer.getData(DraggedEditorIdentifier.prototype);
1144+
if (editorIdentifiers !== undefined) {
11451145

1146-
this.onDrop(e, tabIndex, tabsContainer);
1146+
const draggedEditorIdentifier = editorIdentifiers[0].identifier;
1147+
const sourceGroup = this.editorPartsView.getGroup(draggedEditorIdentifier.groupId);
1148+
if (sourceGroup?.id === this.groupView.id) {
1149+
1150+
const editorIndex = sourceGroup.getIndexOfEditor(draggedEditorIdentifier.editor);
1151+
if (editorIndex < targetIndex) {
1152+
targetIndex--;
1153+
}
1154+
}
1155+
}
1156+
1157+
this.onDrop(e, targetIndex, tabsContainer);
11471158
}
11481159
}));
11491160

@@ -1174,14 +1185,18 @@ export class MultiEditorTabsControl extends EditorTabsControl {
11741185
return false;
11751186
}
11761187

1177-
private updateDropFeedback(element: HTMLElement, isDND: boolean, tabIndex?: number): void {
1188+
private updateDropFeedback(element: HTMLElement, isDND: boolean, e: DragEvent, tabIndex?: number): void {
11781189
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);
11811190

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) || '';
1191+
if (isDND) {
1192+
if (isTab) {
1193+
this.dndDropTarget = this.computeDropTarget(e, tabIndex, element);
1194+
} else {
1195+
this.dndDropTarget = { leftElement: element.lastElementChild as HTMLElement, rightElement: undefined };
1196+
}
1197+
} else {
1198+
this.dndDropTarget = undefined;
1199+
}
11851200

11861201
// Outline
11871202
const activeContrastBorderColor = this.getColor(activeContrastBorder);
@@ -1198,6 +1213,34 @@ export class MultiEditorTabsControl extends EditorTabsControl {
11981213
}
11991214
}
12001215

1216+
private isHeadOfTab(e: DragEvent, tab: HTMLElement): boolean {
1217+
const rect = tab.getBoundingClientRect();
1218+
const offsetXRelativeToParent = e.clientX - rect.left;
1219+
return offsetXRelativeToParent <= rect.width / 2;
1220+
}
1221+
1222+
private computeDropTarget(e: DragEvent, tabIndex: number, targetTab: HTMLElement): { leftElement: HTMLElement | undefined; rightElement: HTMLElement | undefined } | undefined {
1223+
const isHeadOfTab = this.isHeadOfTab(e, targetTab);
1224+
const isLastTab = tabIndex === this.tabsModel.count - 1;
1225+
const isFirstTab = tabIndex === 0;
1226+
1227+
// Before first tab
1228+
if (isHeadOfTab && isFirstTab) {
1229+
return { leftElement: undefined, rightElement: targetTab };
1230+
}
1231+
1232+
// After last tab
1233+
if (!isHeadOfTab && isLastTab) {
1234+
return { leftElement: targetTab, rightElement: undefined };
1235+
}
1236+
1237+
// Between two tabs
1238+
const tabBefore = isHeadOfTab ? targetTab.previousElementSibling : targetTab;
1239+
const tabAfter = isHeadOfTab ? targetTab : targetTab.nextElementSibling;
1240+
1241+
return { leftElement: tabBefore as HTMLElement, rightElement: tabAfter as HTMLElement };
1242+
}
1243+
12011244
private computeTabLabels(): void {
12021245
const { labelFormat } = this.groupsView.partOptions;
12031246
const { verbosity, shortenDuplicates } = this.getLabelConfigFlags(labelFormat);
@@ -2044,7 +2087,7 @@ export class MultiEditorTabsControl extends EditorTabsControl {
20442087
private async onDrop(e: DragEvent, targetTabIndex: number, tabsContainer: HTMLElement): Promise<void> {
20452088
EventHelper.stop(e, true);
20462089

2047-
this.updateDropFeedback(tabsContainer, false);
2090+
this.updateDropFeedback(tabsContainer, false, e, targetTabIndex);
20482091
tabsContainer.classList.remove('scroll');
20492092

20502093
const targetEditorIndex = this.tabsModel instanceof UnstickyEditorGroupModel ? targetTabIndex + this.groupView.stickyCount : targetTabIndex;
@@ -2367,4 +2410,16 @@ registerThemingParticipant((theme, collector) => {
23672410
collector.addRule(makeTabBackgroundRule(adjustedColor, adjustedColorDrag, false, false));
23682411
}
23692412
}
2413+
2414+
const tabDndIndicatorColor = theme.getColor(TAB_Drag_And_Drop_Between_Indicator);
2415+
if (tabDndIndicatorColor) {
2416+
// DnD Feedback
2417+
2418+
collector.addRule(`
2419+
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.drop-target-left::after,
2420+
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.drop-target-right::before {
2421+
background-color: ${tabDndIndicatorColor};
2422+
}
2423+
`);
2424+
}
23702425
});

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 Indicator
186+
187+
export const TAB_Drag_And_Drop_Between_Indicator = registerColor('tab.dragAndDropBetweenIndicator', {
188+
dark: TAB_INACTIVE_FOREGROUND,
189+
light: TAB_INACTIVE_FOREGROUND,
190+
hcDark: activeContrastBorder,
191+
hcLight: activeContrastBorder
192+
}, localize('tabDragAndDropBetweenIndicator', "Indicator between tabs to indicate that a tab can be dropped on the editor group 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)