Skip to content

Commit

Permalink
status - dynamic hover delay based on content (microsoft#241590)
Browse files Browse the repository at this point in the history
* status - dynamic hover delay based on content

* fix tests
  • Loading branch information
bpasero authored Feb 22, 2025
1 parent 9045879 commit 8cfb2b0
Show file tree
Hide file tree
Showing 15 changed files with 45 additions and 23 deletions.
4 changes: 2 additions & 2 deletions src/vs/base/browser/ui/hover/hoverDelegate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import type { IHoverWidget, IManagedHoverOptions } from './hover.js';
import type { IHoverWidget, IManagedHoverContentOrFactory, IManagedHoverOptions } from './hover.js';
import { HoverPosition } from './hoverWidget.js';
import { IMarkdownString } from '../../../common/htmlContent.js';
import { IDisposable } from '../../../common/lifecycle.js';
Expand Down Expand Up @@ -67,7 +67,7 @@ export interface IHoverDelegateOptions extends IManagedHoverOptions {
export interface IHoverDelegate {
showHover(options: IHoverDelegateOptions, focus?: boolean): IHoverWidget | undefined;
onDidHideHover?: () => void;
delay: number;
delay: number | ((content?: IManagedHoverContentOrFactory) => number);
placement?: 'mouse' | 'element';
showNativeHover?: boolean; // TODO@benibenj remove this, only temp fix for contextviews
}
Expand Down
4 changes: 2 additions & 2 deletions src/vs/editor/browser/services/hoverService/hoverService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -440,7 +440,7 @@ export class HoverService extends Disposable implements IHoverService {
return; // Do not show hover when the mouse is over another hover target
}

mouseOverStore.add(triggerShowHover(hoverDelegate.delay, false, target));
mouseOverStore.add(triggerShowHover(typeof hoverDelegate.delay === 'function' ? hoverDelegate.delay(content) : hoverDelegate.delay, false, target));
}, true));

const onFocus = () => {
Expand All @@ -454,7 +454,7 @@ export class HoverService extends Disposable implements IHoverService {
const toDispose: DisposableStore = new DisposableStore();
const onBlur = () => hideHover(true, true);
toDispose.add(addDisposableListener(targetElement, EventType.BLUR, onBlur, true));
toDispose.add(triggerShowHover(hoverDelegate.delay, false, target));
toDispose.add(triggerShowHover(typeof hoverDelegate.delay === 'function' ? hoverDelegate.delay(content) : hoverDelegate.delay, false, target));
hoverPreparation = toDispose;
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ class DiffToolBar extends Disposable implements IGutterItemView {
const hoverDelegate = this._register(instantiationService.createInstance(
WorkbenchHoverDelegate,
'element',
true,
{ instantHover: true },
{ position: { hoverPosition: HoverPosition.RIGHT } }
));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,7 @@ class MarkdownRenderedHoverParts implements IRenderedHoverParts<MarkdownHover> {
const isActionIncrease = action === HoverVerbosityAction.Increase;
const actionElement = dom.append(container, $(ThemeIcon.asCSSSelector(isActionIncrease ? increaseHoverVerbosityIcon : decreaseHoverVerbosityIcon)));
actionElement.tabIndex = 0;
const hoverDelegate = new WorkbenchHoverDelegate('mouse', false, { target: container, position: { hoverPosition: HoverPosition.LEFT } }, this._configurationService, this._hoverService);
const hoverDelegate = new WorkbenchHoverDelegate('mouse', undefined, { target: container, position: { hoverPosition: HoverPosition.LEFT } }, this._configurationService, this._hoverService);
store.add(this._hoverService.setupManagedHover(hoverDelegate, actionElement, labelForHoverVerbosityAction(this._keybindingService, action)));
if (!actionEnabled) {
actionElement.classList.add('disabled');
Expand Down
2 changes: 1 addition & 1 deletion src/vs/editor/standalone/browser/standaloneCodeEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@ export class StandaloneCodeEditor extends CodeEditorWidget implements IStandalon

createAriaDomNode(options.ariaContainerElement);

setHoverDelegateFactory((placement, enableInstantHover) => instantiationService.createInstance(WorkbenchHoverDelegate, placement, enableInstantHover, {}));
setHoverDelegateFactory((placement, enableInstantHover) => instantiationService.createInstance(WorkbenchHoverDelegate, placement, { instantHover: enableInstantHover }, {}));
setBaseLayerHoverDelegate(hoverService);
}

Expand Down
22 changes: 16 additions & 6 deletions src/vs/platform/hover/browser/hover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,32 +9,42 @@ import { IHoverDelegate, IHoverDelegateOptions } from '../../../base/browser/ui/
import { IConfigurationService } from '../../configuration/common/configuration.js';
import { addStandardDisposableListener, isHTMLElement } from '../../../base/browser/dom.js';
import { KeyCode } from '../../../base/common/keyCodes.js';
import type { IHoverDelegate2, IHoverOptions, IHoverWidget } from '../../../base/browser/ui/hover/hover.js';
import type { IHoverDelegate2, IHoverOptions, IHoverWidget, IManagedHoverContentOrFactory } from '../../../base/browser/ui/hover/hover.js';

export const IHoverService = createDecorator<IHoverService>('hoverService');

export interface IHoverService extends IHoverDelegate2 {
readonly _serviceBrand: undefined;
}

export interface IHoverDelayOptions {
readonly instantHover?: boolean;
readonly dynamicDelay?: (content?: IManagedHoverContentOrFactory) => number | undefined;
}

export class WorkbenchHoverDelegate extends Disposable implements IHoverDelegate {

private lastHoverHideTime = 0;
private timeLimit = 200;

private _delay: number;
get delay(): number {
get delay(): number | ((content: IManagedHoverContentOrFactory) => number) {
if (this.isInstantlyHovering()) {
return 0; // show instantly when a hover was recently shown
}

if (this.hoverOptions?.dynamicDelay) {
return content => this.hoverOptions?.dynamicDelay?.(content) ?? this._delay;
}

return this._delay;
}

private readonly hoverDisposables = this._register(new DisposableStore());

constructor(
public readonly placement: 'mouse' | 'element',
private readonly instantHover: boolean,
private readonly hoverOptions: IHoverDelayOptions | undefined,
private overrideOptions: Partial<IHoverOptions> | ((options: IHoverDelegateOptions, focus?: boolean) => Partial<IHoverOptions>) = {},
@IConfigurationService private readonly configurationService: IConfigurationService,
@IHoverService private readonly hoverService: IHoverService,
Expand Down Expand Up @@ -87,19 +97,19 @@ export class WorkbenchHoverDelegate extends Disposable implements IHoverDelegate
}

private isInstantlyHovering(): boolean {
return this.instantHover && Date.now() - this.lastHoverHideTime < this.timeLimit;
return !!this.hoverOptions?.instantHover && Date.now() - this.lastHoverHideTime < this.timeLimit;
}

setInstantHoverTimeLimit(timeLimit: number): void {
if (!this.instantHover) {
if (!this.hoverOptions?.instantHover) {
throw new Error('Instant hover is not enabled');
}
this.timeLimit = timeLimit;
}

onDidHideHover(): void {
this.hoverDisposables.clear();
if (this.instantHover) {
if (this.hoverOptions?.instantHover) {
this.lastHoverHideTime = Date.now();
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/vs/platform/quickinput/browser/quickInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1296,7 +1296,7 @@ export class QuickInputHoverDelegate extends WorkbenchHoverDelegate {
@IConfigurationService configurationService: IConfigurationService,
@IHoverService hoverService: IHoverService
) {
super('element', false, (options) => this.getOverrideOptions(options), configurationService, hoverService);
super('element', undefined, (options) => this.getOverrideOptions(options), configurationService, hoverService);
}

private getOverrideOptions(options: IHoverDelegateOptions): Partial<IHoverOptions> {
Expand Down
2 changes: 1 addition & 1 deletion src/vs/platform/quickinput/browser/quickInputTree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -929,7 +929,7 @@ export class QuickInputTree extends Disposable {
}

private _registerHoverListeners() {
const delayer = this._register(new ThrottledDelayer(this.hoverDelegate.delay));
const delayer = this._register(new ThrottledDelayer(typeof this.hoverDelegate.delay === 'function' ? this.hoverDelegate.delay() : this.hoverDelegate.delay));
this._register(this._tree.onMouseOver(async e => {
// If we hover over an anchor element, we don't want to show the hover because
// the anchor may have a tooltip that we want to show instead.
Expand Down
16 changes: 14 additions & 2 deletions src/vs/workbench/browser/parts/statusbar/statusbarPart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { IThemeService } from '../../../../platform/theme/common/themeService.js
import { STATUS_BAR_BACKGROUND, STATUS_BAR_FOREGROUND, STATUS_BAR_NO_FOLDER_BACKGROUND, STATUS_BAR_ITEM_HOVER_BACKGROUND, STATUS_BAR_BORDER, STATUS_BAR_NO_FOLDER_FOREGROUND, STATUS_BAR_NO_FOLDER_BORDER, STATUS_BAR_ITEM_COMPACT_HOVER_BACKGROUND, STATUS_BAR_ITEM_FOCUS_BORDER, STATUS_BAR_FOCUS_BORDER } from '../../../common/theme.js';
import { IWorkspaceContextService, WorkbenchState } from '../../../../platform/workspace/common/workspace.js';
import { contrastBorder, activeContrastBorder } from '../../../../platform/theme/common/colorRegistry.js';
import { EventHelper, addDisposableListener, EventType, clearNode, getWindow } from '../../../../base/browser/dom.js';
import { EventHelper, addDisposableListener, EventType, clearNode, getWindow, isHTMLElement } from '../../../../base/browser/dom.js';
import { createStyleSheet } from '../../../../base/browser/domStylesheets.js';
import { IStorageService } from '../../../../platform/storage/common/storage.js';
import { Parts, IWorkbenchLayoutService } from '../../../services/layout/browser/layoutService.js';
Expand Down Expand Up @@ -145,7 +145,19 @@ class StatusbarPart extends Part implements IStatusbarEntryContainer {
private leftItemsContainer: HTMLElement | undefined;
private rightItemsContainer: HTMLElement | undefined;

private readonly hoverDelegate = this._register(this.instantiationService.createInstance(WorkbenchHoverDelegate, 'element', true, (_, focus?: boolean) => (
private readonly hoverDelegate = this._register(this.instantiationService.createInstance(WorkbenchHoverDelegate, 'element', {
instantHover: true,
dynamicDelay(content) {
if (typeof content === 'function' || isHTMLElement(content)) {
// override the delay for content that is rich (e.g. html or long running)
// so that is appears more instantly. these hovers carry more important
// information and should not be delayed by preference.
return 500;
}

return undefined;
}
}, (_, focus?: boolean) => (
{
persistence: {
hideOnKeyDown: true,
Expand Down
2 changes: 1 addition & 1 deletion src/vs/workbench/browser/parts/views/treeView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1265,7 +1265,7 @@ class TreeRenderer extends Disposable implements ITreeRenderer<ITreeItem, FuzzyS
@IInstantiationService instantiationService: IInstantiationService,
) {
super();
this._hoverDelegate = this._register(instantiationService.createInstance(WorkbenchHoverDelegate, 'mouse', false, {}));
this._hoverDelegate = this._register(instantiationService.createInstance(WorkbenchHoverDelegate, 'mouse', undefined, {}));
this._register(this.themeService.onDidFileIconThemeChange(() => this.rerender()));
this._register(this.themeService.onDidColorThemeChange(() => this.rerender()));
this._register(checkboxStateHandler.onDidChangeCheckboxState(items => {
Expand Down
2 changes: 1 addition & 1 deletion src/vs/workbench/browser/workbench.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ export class Workbench extends Layout {

// Default Hover Delegate must be registered before creating any workbench/layout components
// as these possibly will use the default hover delegate
setHoverDelegateFactory((placement, enableInstantHover) => instantiationService.createInstance(WorkbenchHoverDelegate, placement, enableInstantHover, {}));
setHoverDelegateFactory((placement, enableInstantHover) => instantiationService.createInstance(WorkbenchHoverDelegate, placement, { instantHover: enableInstantHover }, {}));
setBaseLayerHoverDelegate(hoverService);

// Layout
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,7 @@ export class NotebookEditorWorkbenchToolbar extends Disposable {

// Make sure both toolbars have the same hover delegate for instant hover to work
// Due to the elements being further apart than normal toolbars, the default time limit is to short and has to be increased
const hoverDelegate = this._register(this.instantiationService.createInstance(WorkbenchHoverDelegate, 'element', true, {}));
const hoverDelegate = this._register(this.instantiationService.createInstance(WorkbenchHoverDelegate, 'element', { instantHover: true }, {}));
hoverDelegate.setInstantHoverTimeLimit(600);

const leftToolbarOptions: IWorkbenchToolBarOptions = {
Expand Down
2 changes: 1 addition & 1 deletion src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts
Original file line number Diff line number Diff line change
Expand Up @@ -625,7 +625,7 @@ class HistoryItemHoverDelegate extends WorkbenchHoverDelegate {
@IHoverService hoverService: IHoverService,

) {
super('element', true, () => this.getHoverOptions(), configurationService, hoverService);
super('element', { instantHover: true }, () => this.getHoverOptions(), configurationService, hoverService);
}

private getHoverOptions(): Partial<IHoverOptions> {
Expand Down
2 changes: 1 addition & 1 deletion src/vs/workbench/contrib/timeline/browser/timelinePane.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1162,7 +1162,7 @@ class TimelineTreeRenderer implements ITreeRenderer<TreeElement, FuzzyScore, Tim
@IThemeService private themeService: IThemeService,
) {
this.actionViewItemProvider = createActionViewItem.bind(undefined, this.instantiationService);
this._hoverDelegate = this.instantiationService.createInstance(WorkbenchHoverDelegate, 'element', true, {
this._hoverDelegate = this.instantiationService.createInstance(WorkbenchHoverDelegate, 'element', { instantHover: true }, {
position: {
hoverPosition: HoverPosition.RIGHT // Will flip when there's no space
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1891,7 +1891,7 @@ class ProfileResourceChildTreeItemRenderer extends AbstractProfileResourceTreeRe
) {
super();
this.labels = instantiationService.createInstance(ResourceLabels, DEFAULT_LABELS_CONTAINER);
this.hoverDelegate = this._register(instantiationService.createInstance(WorkbenchHoverDelegate, 'mouse', false, {}));
this.hoverDelegate = this._register(instantiationService.createInstance(WorkbenchHoverDelegate, 'mouse', undefined, {}));
}

renderTemplate(parent: HTMLElement): IProfileResourceChildTreeItemTemplateData {
Expand Down

0 comments on commit 8cfb2b0

Please sign in to comment.