Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added Tab Drag and Drop Indicator #203681

Merged
merged 8 commits into from
Feb 1, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -220,14 +220,6 @@
padding-right: 5px; /* we need less room when sizing is shrink/fixed */
}

.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.dragged {
transform: translate3d(0px, 0px, 0px); /* forces tab to be drawn on a separate layer (fixes https://github.com/microsoft/vscode/issues/18733) */
}

.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.dragged-over div {
pointer-events: none; /* prevents cursor flickering (fixes https://github.com/microsoft/vscode/issues/38753) */
}

.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.tab-actions-left:not(.sticky-compact) {
flex-direction: row-reverse;
padding-left: 0;
Expand Down Expand Up @@ -466,3 +458,41 @@
/* When multiple tab bars are visible, only show editor actions for the last tab bar */
display: none;
}

/* Drag and drop target */

.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.drop-target-left::after ,
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.drop-target-right::before {
content: "";
position: absolute;
top: 0;
height: 100%;
width: 1px;
pointer-events: none;
}

.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.drop-target-right::before {
left: 0;
}

.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.drop-target-left::after {
right: -1px; /* -1 to connect with drop-target-right */
}

/* Make drop target edge cases more visible (wrapped tabs & first/last) */

.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.last-in-row.drop-target-left:not(:last-child)::after {
right: 1px;
}

.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.drop-target-right:first-child:before,
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.last-in-row + .tab.drop-target-right::before {
left: 1px;
}

.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.last-in-row.drop-target-left::after,
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.last-in-row + .tab.drop-target-right::before,
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab:last-child.drop-target-left::after,
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab:first-child.drop-target-right::before {
width: 2px;
}
165 changes: 110 additions & 55 deletions src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import { ScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElemen
import { ScrollbarVisibility } from 'vs/base/common/scrollable';
import { getOrSet } from 'vs/base/common/map';
import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
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';
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_BETWEEEN_INDICATOR } from 'vs/workbench/common/theme';
import { activeContrastBorder, contrastBorder, editorBackground } from 'vs/platform/theme/common/colorRegistry';
import { ResourcesDropHandler, DraggedEditorIdentifier, DraggedEditorGroupIdentifier, extractTreeDropData, isWindowDraggedOver } from 'vs/workbench/browser/dnd';
import { Color } from 'vs/base/common/color';
Expand Down Expand Up @@ -132,6 +132,29 @@ export class MultiEditorTabsControl extends EditorTabsControl {
private lastMouseWheelEventTime = 0;
private isMouseOverTabs = false;

private _dndDropTarget: { leftElement: HTMLElement | undefined; rightElement: HTMLElement | undefined } | undefined;
private set dndDropTarget(target: { leftElement: HTMLElement | undefined; rightElement: HTMLElement | undefined } | undefined) {
const oldTargets = this._dndDropTarget;
if (oldTargets === target || oldTargets && target && oldTargets.leftElement === target.leftElement && oldTargets.rightElement === target.rightElement) {
return;
}

const dropClassLeft = 'drop-target-left';
const dropClassRight = 'drop-target-right';

if (oldTargets) {
oldTargets.leftElement?.classList.remove(dropClassLeft);
oldTargets.rightElement?.classList.remove(dropClassRight);
}

if (target) {
target.leftElement?.classList.add(dropClassLeft);
target.rightElement?.classList.add(dropClassRight);
}

this._dndDropTarget = target;
}

constructor(
parent: HTMLElement,
editorPartsView: IEditorPartsView,
Expand Down Expand Up @@ -335,7 +358,6 @@ export class MultiEditorTabsControl extends EditorTabsControl {

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

Expand All @@ -352,18 +374,6 @@ export class MultiEditorTabsControl extends EditorTabsControl {
let isLocalDragAndDrop = false;
if (this.editorTransfer.hasData(DraggedEditorIdentifier.prototype)) {
isLocalDragAndDrop = true;

const data = this.editorTransfer.getData(DraggedEditorIdentifier.prototype);
if (Array.isArray(data)) {
const localDraggedEditor = data[0].identifier;
if (this.groupView.id === localDraggedEditor.groupId && this.tabsModel.isLast(localDraggedEditor.editor)) {
if (e.dataTransfer) {
e.dataTransfer.dropEffect = 'none';
}

return;
}
}
}

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

this.updateDropFeedback(tabsContainer, true);
this.updateDropFeedback(tabsContainer, true, e);
},

onDragLeave: e => {
this.updateDropFeedback(tabsContainer, false);
this.updateDropFeedback(tabsContainer, false, e);
tabsContainer.classList.remove('scroll');
},

onDragEnd: e => {
this.updateDropFeedback(tabsContainer, false);
this.updateDropFeedback(tabsContainer, false, e);
tabsContainer.classList.remove('scroll');

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

onDrop: e => {
this.updateDropFeedback(tabsContainer, false);
this.updateDropFeedback(tabsContainer, false, e);
tabsContainer.classList.remove('scroll');

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

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

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

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

onDrag: e => {
lastDragEvent = e;
},

onDragEnter: e => {

// Update class to signal drag operation
tab.classList.add('dragged-over');

// Return if transfer is unsupported
if (!this.isSupportedDropTransfer(e)) {
if (e.dataTransfer) {
Expand All @@ -1066,22 +1073,9 @@ export class MultiEditorTabsControl extends EditorTabsControl {
return;
}

// Return if dragged editor is the current tab dragged over
let isLocalDragAndDrop = false;
if (this.editorTransfer.hasData(DraggedEditorIdentifier.prototype)) {
isLocalDragAndDrop = true;

const data = this.editorTransfer.getData(DraggedEditorIdentifier.prototype);
if (Array.isArray(data)) {
const localDraggedEditor = data[0].identifier;
if (localDraggedEditor.editor === this.tabsModel.getEditorByIndex(tabIndex) && localDraggedEditor.groupId === this.groupView.id) {
if (e.dataTransfer) {
e.dataTransfer.dropEffect = 'none';
}

return;
}
}
}

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

this.updateDropFeedback(tab, true, tabIndex);
this.updateDropFeedback(tab, true, e, tabIndex);
},

onDragOver: (_, dragDuration) => {
onDragOver: (e, dragDuration) => {
if (dragDuration >= MultiEditorTabsControl.DRAG_OVER_OPEN_TAB_THRESHOLD) {
const draggedOverTab = this.tabsModel.getEditorByIndex(tabIndex);
if (draggedOverTab && this.tabsModel.activeEditor !== draggedOverTab) {
this.groupView.openEditor(draggedOverTab, { preserveFocus: true });
}
}
},

onDragLeave: () => {
tab.classList.remove('dragged-over');
this.updateDropFeedback(tab, false, tabIndex);
if (e.dataTransfer?.dropEffect !== 'none') {
this.updateDropFeedback(tab, true, e, tabIndex);
}
},

onDragEnd: async e => {
tab.classList.remove('dragged-over');
this.updateDropFeedback(tab, false, tabIndex);
this.updateDropFeedback(tab, false, e, tabIndex);

this.editorTransfer.clearData(DraggedEditorIdentifier.prototype);

Expand Down Expand Up @@ -1140,10 +1132,29 @@ export class MultiEditorTabsControl extends EditorTabsControl {
},

onDrop: e => {
tab.classList.remove('dragged-over');
this.updateDropFeedback(tab, false, tabIndex);
this.updateDropFeedback(tab, false, e, tabIndex);

// compute the target index
let targetIndex = tabIndex;
if (!this.isHeadOfTab(e, tab)) {
targetIndex++;
}

const editorIdentifiers = this.editorTransfer.getData(DraggedEditorIdentifier.prototype);
if (editorIdentifiers !== undefined) {

this.onDrop(e, tabIndex, tabsContainer);
const draggedEditorIdentifier = editorIdentifiers[0].identifier;
const sourceGroup = this.editorPartsView.getGroup(draggedEditorIdentifier.groupId);
if (sourceGroup?.id === this.groupView.id) {

const editorIndex = sourceGroup.getIndexOfEditor(draggedEditorIdentifier.editor);
if (editorIndex < targetIndex) {
targetIndex--;
}
}
}

this.onDrop(e, targetIndex, tabsContainer);
}
}));

Expand Down Expand Up @@ -1174,14 +1185,18 @@ export class MultiEditorTabsControl extends EditorTabsControl {
return false;
}

private updateDropFeedback(element: HTMLElement, isDND: boolean, tabIndex?: number): void {
private updateDropFeedback(element: HTMLElement, isDND: boolean, e: DragEvent, tabIndex?: number): void {
const isTab = (typeof tabIndex === 'number');
const editor = typeof tabIndex === 'number' ? this.tabsModel.getEditorByIndex(tabIndex) : undefined;
const isActiveTab = isTab && !!editor && this.tabsModel.isActive(editor);

// Background
const noDNDBackgroundColor = isTab ? this.getColor(isActiveTab ? TAB_ACTIVE_BACKGROUND : TAB_INACTIVE_BACKGROUND) : '';
element.style.backgroundColor = (isDND ? this.getColor(EDITOR_DRAG_AND_DROP_BACKGROUND) : noDNDBackgroundColor) || '';
if (isDND) {
if (isTab) {
this.dndDropTarget = this.computeDropTarget(e, tabIndex, element);
} else {
this.dndDropTarget = { leftElement: element.lastElementChild as HTMLElement, rightElement: undefined };
}
} else {
this.dndDropTarget = undefined;
}

// Outline
const activeContrastBorderColor = this.getColor(activeContrastBorder);
Expand All @@ -1198,6 +1213,34 @@ export class MultiEditorTabsControl extends EditorTabsControl {
}
}

private isHeadOfTab(e: DragEvent, tab: HTMLElement): boolean {
const rect = tab.getBoundingClientRect();
const offsetXRelativeToParent = e.clientX - rect.left;
return offsetXRelativeToParent <= rect.width / 2;
}

private computeDropTarget(e: DragEvent, tabIndex: number, targetTab: HTMLElement): { leftElement: HTMLElement | undefined; rightElement: HTMLElement | undefined } | undefined {
const isHeadOfTab = this.isHeadOfTab(e, targetTab);
const isLastTab = tabIndex === this.tabsModel.count - 1;
const isFirstTab = tabIndex === 0;

// Before first tab
if (isHeadOfTab && isFirstTab) {
return { leftElement: undefined, rightElement: targetTab };
}

// After last tab
if (!isHeadOfTab && isLastTab) {
return { leftElement: targetTab, rightElement: undefined };
}

// Between two tabs
const tabBefore = isHeadOfTab ? targetTab.previousElementSibling : targetTab;
const tabAfter = isHeadOfTab ? targetTab : targetTab.nextElementSibling;

return { leftElement: tabBefore as HTMLElement, rightElement: tabAfter as HTMLElement };
}

private computeTabLabels(): void {
const { labelFormat } = this.groupsView.partOptions;
const { verbosity, shortenDuplicates } = this.getLabelConfigFlags(labelFormat);
Expand Down Expand Up @@ -2044,7 +2087,7 @@ export class MultiEditorTabsControl extends EditorTabsControl {
private async onDrop(e: DragEvent, targetTabIndex: number, tabsContainer: HTMLElement): Promise<void> {
EventHelper.stop(e, true);

this.updateDropFeedback(tabsContainer, false);
this.updateDropFeedback(tabsContainer, false, e, targetTabIndex);
tabsContainer.classList.remove('scroll');

const targetEditorIndex = this.tabsModel instanceof UnstickyEditorGroupModel ? targetTabIndex + this.groupView.stickyCount : targetTabIndex;
Expand Down Expand Up @@ -2367,4 +2410,16 @@ registerThemingParticipant((theme, collector) => {
collector.addRule(makeTabBackgroundRule(adjustedColor, adjustedColorDrag, false, false));
}
}

const tabDndIndicatorColor = theme.getColor(TAB_DRAG_AND_DROP_BETWEEEN_INDICATOR);
if (tabDndIndicatorColor) {
// DnD Feedback

collector.addRule(`
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.drop-target-left::after,
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.drop-target-right::before {
background-color: ${tabDndIndicatorColor};
}
`);
}
});
11 changes: 11 additions & 0 deletions src/vs/workbench/common/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,17 @@ export const TAB_UNFOCUSED_HOVER_BORDER = registerColor('tab.unfocusedHoverBorde

//#endregion

//#region Tab Drag and Drop Indicator

export const TAB_DRAG_AND_DROP_BETWEEEN_INDICATOR = registerColor('tab.dragAndDropBetweenIndicator', {
dark: TAB_INACTIVE_FOREGROUND,
light: TAB_INACTIVE_FOREGROUND,
hcDark: activeContrastBorder,
hcLight: activeContrastBorder
}, localize('tabDragAndDropBetweenIndicator', "Indicator between tabs to indicate that a tab can be dropped 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."));

//#endregion

//#region Tab Modified Border

export const TAB_ACTIVE_MODIFIED_BORDER = registerColor('tab.activeModifiedBorder', {
Expand Down
Loading