Skip to content

Commit

Permalink
Merge pull request #161635 from microsoft/rebornix/grotesque-cat
Browse files Browse the repository at this point in the history
Overview ruler for notebook diff editor
  • Loading branch information
rebornix committed Sep 26, 2022
2 parents f0f4c87 + a609f1e commit eeaee50
Show file tree
Hide file tree
Showing 8 changed files with 334 additions and 17 deletions.
8 changes: 6 additions & 2 deletions src/vs/base/browser/ui/list/listView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -406,8 +406,12 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
}
}

triggerScrollFromMouseWheelEvent(browserEvent: IMouseWheelEvent) {
this.scrollableElement.triggerScrollFromMouseWheelEvent(browserEvent);
delegateScrollFromMouseWheelEvent(browserEvent: IMouseWheelEvent) {
this.scrollableElement.delegateScrollFromMouseWheelEvent(browserEvent);
}

delegateVerticalScrollbarPointerDown(browserEvent: PointerEvent) {
this.scrollableElement.delegateVerticalScrollbarPointerDown(browserEvent);
}

updateElementHeight(index: number, size: number | undefined, anchorIndex: number | null): void {
Expand Down
2 changes: 1 addition & 1 deletion src/vs/base/browser/ui/scrollbar/scrollableElement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,7 @@ export abstract class AbstractScrollableElement extends Widget {
this._revealOnScroll = value;
}

public triggerScrollFromMouseWheelEvent(browserEvent: IMouseWheelEvent) {
public delegateScrollFromMouseWheelEvent(browserEvent: IMouseWheelEvent) {
this._onMouseWheel(new StandardWheelEvent(browserEvent));
}

Expand Down
29 changes: 24 additions & 5 deletions src/vs/workbench/contrib/notebook/browser/diff/notebookDiff.css
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@
width: 50%;
} */

.notebook-text-diff-editor {
position: relative;
}

.notebook-text-diff-editor .cell-body {
display: flex;
flex-direction: row;
Expand Down Expand Up @@ -61,7 +65,7 @@
/* overflow: hidden; */
}

.notebook-text-diff-editor > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row {
.notebook-text-diff-editor > .notebook-diff-list-view > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row {
cursor: default;
}

Expand Down Expand Up @@ -142,13 +146,13 @@
overflow: hidden;
}

.monaco-workbench .notebook-text-diff-editor > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row {
.monaco-workbench .notebook-text-diff-editor > .notebook-diff-list-view > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row {
overflow: visible !important;
}

.monaco-workbench .notebook-text-diff-editor > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row,
.monaco-workbench .notebook-text-diff-editor > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row:hover,
.monaco-workbench .notebook-text-diff-editor > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused {
.monaco-workbench .notebook-text-diff-editor > .notebook-diff-list-view > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row,
.monaco-workbench .notebook-text-diff-editor > .notebook-diff-list-view > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row:hover,
.monaco-workbench .notebook-text-diff-editor > .notebook-diff-list-view > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused {
outline: none !important;
background-color: transparent !important;
}
Expand Down Expand Up @@ -287,3 +291,18 @@
left: 4px !important;
width: 15px !important;
}

.monaco-workbench .notebook-text-diff-editor > .monaco-list > .monaco-scrollable-element > .scrollbar.visible {
z-index: var(--z-index-notebook-scrollbar);
cursor: default;
}

.notebook-text-diff-editor .notebook-overview-ruler-container {
position: absolute;
top: 0;
right: 0;
}

.notebook-text-diff-editor .notebook-overview-ruler-container .diffViewport {
z-index: var(--notebook-diff-view-viewport-slider);
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,12 @@ export interface INotebookTextDiffEditor {
notebookOptions: NotebookOptions;
readonly textModel?: NotebookTextModel;
onMouseUp: Event<{ readonly event: MouseEvent; readonly target: DiffElementViewModelBase }>;
onDidScroll: Event<void>;
onDidDynamicOutputRendered: Event<{ cell: IGenericCellViewModel; output: ICellOutputViewModel }>;
getOverflowContainerDomNode(): HTMLElement;
getLayoutInfo(): NotebookLayoutInfo;
getScrollTop(): number;
getScrollHeight(): number;
layoutNotebookCell(cell: DiffElementViewModelBase, height: number): void;
createOutput(cellDiffViewModel: DiffElementViewModelBase, cellViewModel: IDiffNestedCellViewModel, output: IInsetRenderOutput, getOffset: () => number, diffSide: DiffSide): void;
showInset(cellDiffViewModel: DiffElementViewModelBase, cellViewModel: IDiffNestedCellViewModel, displayOutput: ICellOutputViewModel, diffSide: DiffSide): void;
Expand All @@ -42,6 +45,7 @@ export interface INotebookTextDiffEditor {
* Trigger the editor to scroll from scroll event programmatically
*/
triggerScroll(event: IMouseWheelEvent): void;
delegateVerticalScrollbarPointerDown(browserEvent: PointerEvent): void;
getCellByInfo(cellInfo: ICommonCellInfo): IGenericCellViewModel;
focusNotebookCell(cell: IGenericCellViewModel, focus: 'editor' | 'container' | 'output'): Promise<void>;
focusNextNotebookCell(cell: IGenericCellViewModel, focus: 'editor' | 'container' | 'output'): Promise<void>;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as browser from 'vs/base/browser/browser';
import * as DOM from 'vs/base/browser/dom';
import { createFastDomNode, FastDomNode } from 'vs/base/browser/fastDomNode';
import { Color } from 'vs/base/common/color';
import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle';
import { defaultInsertColor, defaultRemoveColor, diffInserted, diffOverviewRulerInserted, diffOverviewRulerRemoved, diffRemoved, scrollbarSliderActiveBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground } from 'vs/platform/theme/common/colorRegistry';
import { IColorTheme, IThemeService, registerThemingParticipant, Themable } from 'vs/platform/theme/common/themeService';
import { DiffElementViewModelBase } from 'vs/workbench/contrib/notebook/browser/diff/diffElementViewModel';
import { NotebookDiffEditorEventDispatcher } from 'vs/workbench/contrib/notebook/browser/diff/eventDispatcher';
import { INotebookTextDiffEditor } from 'vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser';

export class NotebookDiffOverviewRuler extends Themable {
private readonly _domNode: FastDomNode<HTMLCanvasElement>;
private readonly _overviewViewportDomElement: FastDomNode<HTMLElement>;

private _diffElementViewModels: DiffElementViewModelBase[] = [];
private _lanes = 2;

private _insertColor: Color | null;
private _insertColorHex: string | null;
private _removeColor: Color | null;
private _removeColorHex: string | null;

private _disposables: DisposableStore;
private _renderAnimationFrame: IDisposable | null;

constructor(readonly notebookEditor: INotebookTextDiffEditor, readonly width: number, container: HTMLElement, @IThemeService themeService: IThemeService) {
super(themeService);
this._insertColor = null;
this._removeColor = null;
this._insertColorHex = null;
this._removeColorHex = null;
this._disposables = this._register(new DisposableStore());
this._renderAnimationFrame = null;
this._domNode = createFastDomNode(document.createElement('canvas'));
this._domNode.setPosition('relative');
this._domNode.setLayerHinting(true);
this._domNode.setContain('strict');

container.appendChild(this._domNode.domNode);

this._overviewViewportDomElement = createFastDomNode(document.createElement('div'));
this._overviewViewportDomElement.setClassName('diffViewport');
this._overviewViewportDomElement.setPosition('absolute');
this._overviewViewportDomElement.setWidth(width);
container.appendChild(this._overviewViewportDomElement.domNode);

this._register(browser.PixelRatio.onDidChange(() => {
this._scheduleRender();
}));

this._register(this.themeService.onDidColorThemeChange(e => {
const colorChanged = this.applyColors(e);
if (colorChanged) {
this._scheduleRender();
}
}));
this.applyColors(this.themeService.getColorTheme());

this._register(this.notebookEditor.onDidScroll(() => {
this._renderOverviewViewport();
}));

this._register(DOM.addStandardDisposableListener(this._overviewViewportDomElement.domNode, DOM.EventType.POINTER_DOWN, (e) => {
this.notebookEditor.delegateVerticalScrollbarPointerDown(e);
}));
}

private applyColors(theme: IColorTheme): boolean {
const newInsertColor = theme.getColor(diffOverviewRulerInserted) || (theme.getColor(diffInserted) || defaultInsertColor).transparent(2);
const newRemoveColor = theme.getColor(diffOverviewRulerRemoved) || (theme.getColor(diffRemoved) || defaultRemoveColor).transparent(2);
const hasChanges = !newInsertColor.equals(this._insertColor) || !newRemoveColor.equals(this._removeColor);
this._insertColor = newInsertColor;
this._removeColor = newRemoveColor;
if (this._insertColor) {
this._insertColorHex = Color.Format.CSS.formatHexA(this._insertColor);
}

if (this._removeColor) {
this._removeColorHex = Color.Format.CSS.formatHexA(this._removeColor);
}

return hasChanges;
}

layout() {
this._layoutNow();
}

updateViewModels(elements: DiffElementViewModelBase[], eventDispatcher: NotebookDiffEditorEventDispatcher | undefined) {
this._disposables.clear();

this._diffElementViewModels = elements;

if (eventDispatcher) {
this._disposables.add(eventDispatcher.onDidChangeLayout(() => {
this._scheduleRender();
}));

this._disposables.add(eventDispatcher.onDidChangeCellLayout(() => {
this._scheduleRender();
}));
}

this._scheduleRender();
}

private _scheduleRender(): void {
if (this._renderAnimationFrame === null) {
this._renderAnimationFrame = DOM.runAtThisOrScheduleAtNextAnimationFrame(this._onRenderScheduled.bind(this), 100);
}
}

private _onRenderScheduled(): void {
this._renderAnimationFrame = null;
this._layoutNow();
}

private _layoutNow() {
const layoutInfo = this.notebookEditor.getLayoutInfo();
const height = layoutInfo.height;
const ratio = browser.PixelRatio.value;
this._domNode.setWidth(this.width);
this._domNode.setHeight(height);
this._domNode.domNode.width = this.width * ratio;
this._domNode.domNode.height = height * ratio;
const ctx = this._domNode.domNode.getContext('2d')!;
ctx.clearRect(0, 0, this.width * ratio, height * ratio);
this._renderCanvas(ctx, this.width * ratio, ratio);
this._renderOverviewViewport();
}

private _renderOverviewViewport(): void {
const layout = this._computeOverviewViewport();
if (!layout) {
this._overviewViewportDomElement.setTop(0);
this._overviewViewportDomElement.setHeight(0);
} else {
this._overviewViewportDomElement.setTop(layout.top);
this._overviewViewportDomElement.setHeight(layout.height);
}
}

private _computeOverviewViewport(): { height: number; top: number } | null {
const layoutInfo = this.notebookEditor.getLayoutInfo();
if (!layoutInfo) {
return null;
}

const scrollTop = this.notebookEditor.getScrollTop();
const scrollHeight = this.notebookEditor.getScrollHeight();

const computedAvailableSize = Math.max(0, layoutInfo.height);
const computedRepresentableSize = Math.max(0, computedAvailableSize - 2 * 0);
const computedRatio = scrollHeight > 0 ? (computedRepresentableSize / scrollHeight) : 0;

const computedSliderSize = Math.max(0, Math.floor(layoutInfo.height * computedRatio));
const computedSliderPosition = Math.floor(scrollTop * computedRatio);

return {
height: computedSliderSize,
top: computedSliderPosition
};
}

private _renderCanvas(ctx: CanvasRenderingContext2D, width: number, ratio: number) {
if (!this._insertColorHex || !this._removeColorHex) {
// no op when colors are not yet known
return;
}

const laneWidth = width / this._lanes;
let currentFrom = 0;
for (let i = 0; i < this._diffElementViewModels.length; i++) {
const element = this._diffElementViewModels[i];

const cellHeight = element.layoutInfo.totalHeight * ratio;
switch (element.type) {
case 'insert':
ctx.fillStyle = this._insertColorHex;
ctx.fillRect(laneWidth, currentFrom, laneWidth, cellHeight);
break;
case 'delete':
ctx.fillStyle = this._removeColorHex;
ctx.fillRect(0, currentFrom, laneWidth, cellHeight);
break;
case 'unchanged':
break;
case 'modified':
ctx.fillStyle = this._removeColorHex;
ctx.fillRect(0, currentFrom, laneWidth, cellHeight);
ctx.fillStyle = this._insertColorHex;
ctx.fillRect(laneWidth, currentFrom, laneWidth, cellHeight);
break;
}

currentFrom += cellHeight;
}
}

override dispose() {
if (this._renderAnimationFrame !== null) {
this._renderAnimationFrame.dispose();
this._renderAnimationFrame = null;
}

super.dispose();
}
}

registerThemingParticipant((theme, collector) => {
const scrollbarSliderBackgroundColor = theme.getColor(scrollbarSliderBackground);
if (scrollbarSliderBackgroundColor) {
collector.addRule(`
.notebook-text-diff-editor .diffViewport {
background: ${scrollbarSliderBackgroundColor};
}
`);
}

const scrollbarSliderHoverBackgroundColor = theme.getColor(scrollbarSliderHoverBackground);
if (scrollbarSliderHoverBackgroundColor) {
collector.addRule(`
.notebook-text-diff-editor .diffViewport:hover {
background: ${scrollbarSliderHoverBackgroundColor};
}
`);
}

const scrollbarSliderActiveBackgroundColor = theme.getColor(scrollbarSliderActiveBackground);
if (scrollbarSliderActiveBackgroundColor) {
collector.addRule(`
.notebook-text-diff-editor .diffViewport:active {
background: ${scrollbarSliderActiveBackgroundColor};
}
`);
}
});

0 comments on commit eeaee50

Please sign in to comment.