Path: blob/main/src/vs/platform/hover/browser/hoverWidget.ts
5221 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import './hover.css';6import { Disposable, DisposableStore, IDisposable, MutableDisposable, toDisposable } from '../../../base/common/lifecycle.js';7import { Event, Emitter } from '../../../base/common/event.js';8import * as dom from '../../../base/browser/dom.js';9import { IKeybindingService } from '../../keybinding/common/keybinding.js';10import { KeyCode } from '../../../base/common/keyCodes.js';11import { IConfigurationService } from '../../configuration/common/configuration.js';12import { HoverAction, HoverPosition, HoverWidget as BaseHoverWidget, getHoverAccessibleViewHint } from '../../../base/browser/ui/hover/hoverWidget.js';13import { Widget } from '../../../base/browser/ui/widget.js';14import { AnchorPosition } from '../../../base/browser/ui/contextview/contextview.js';15import { IMarkdownRendererService } from '../../markdown/browser/markdownRenderer.js';16import { isMarkdownString } from '../../../base/common/htmlContent.js';17import { localize } from '../../../nls.js';18import { isMacintosh } from '../../../base/common/platform.js';19import { IAccessibilityService } from '../../accessibility/common/accessibility.js';20import { status } from '../../../base/browser/ui/aria/aria.js';21import { HoverStyle, type IHoverOptions, type IHoverTarget, type IHoverWidget } from '../../../base/browser/ui/hover/hover.js';22import { TimeoutTimer } from '../../../base/common/async.js';23import { isNumber } from '../../../base/common/types.js';2425const $ = dom.$;26type TargetRect = {27left: number;28right: number;29top: number;30bottom: number;31width: number;32height: number;33center: { x: number; y: number };34};3536const enum Constants {37PointerSize = 3,38HoverBorderWidth = 2,39HoverWindowEdgeMargin = 2,40}4142export class HoverWidget extends Widget implements IHoverWidget {43private readonly _messageListeners = new DisposableStore();44private readonly _lockMouseTracker: CompositeMouseTracker;4546private readonly _hover: BaseHoverWidget;47private readonly _hoverPointer: HTMLElement | undefined;48private readonly _hoverContainer: HTMLElement;49private readonly _target: IHoverTarget;50private readonly _linkHandler: ((url: string) => void) | undefined;5152private _isDisposed: boolean = false;53private _hoverPosition: HoverPosition;54private _forcePosition: boolean = false;55private _x: number = 0;56private _y: number = 0;57private _isLocked: boolean = false;58private _enableFocusTraps: boolean = false;59private _addedFocusTrap: boolean = false;60private _maxHeightRatioRelativeToWindow: number = 0.5;6162private get _targetWindow(): Window {63return dom.getWindow(this._target.targetElements[0]);64}65private get _targetDocumentElement(): HTMLElement {66return dom.getWindow(this._target.targetElements[0]).document.documentElement;67}6869get isDisposed(): boolean { return this._isDisposed; }70get isMouseIn(): boolean { return this._lockMouseTracker.isMouseIn; }71get domNode(): HTMLElement { return this._hover.containerDomNode; }7273private readonly _onDispose = this._register(new Emitter<void>());74get onDispose(): Event<void> { return this._onDispose.event; }75private readonly _onRequestLayout = this._register(new Emitter<void>());76get onRequestLayout(): Event<void> { return this._onRequestLayout.event; }7778get anchor(): AnchorPosition { return this._hoverPosition === HoverPosition.BELOW ? AnchorPosition.BELOW : AnchorPosition.ABOVE; }79get x(): number { return this._x; }80get y(): number { return this._y; }8182/**83* Whether the hover is "locked" by holding the alt/option key. When locked, the hover will not84* hide and can be hovered regardless of whether the `hideOnHover` hover option is set.85*/86get isLocked(): boolean { return this._isLocked; }87set isLocked(value: boolean) {88if (this._isLocked === value) {89return;90}91this._isLocked = value;92this._hoverContainer.classList.toggle('locked', this._isLocked);93}9495/**96* Adds an element to be tracked by this hover's mouse tracker. Mouse events on97* this element will be considered as being "inside" the hover, preventing it98* from closing. This is used for nested hovers where the child hover's container99* should be treated as part of the parent hover.100*/101addMouseTrackingElement(element: HTMLElement): IDisposable {102return this._lockMouseTracker.addElement(element);103}104105constructor(106options: IHoverOptions,107@IKeybindingService private readonly _keybindingService: IKeybindingService,108@IConfigurationService private readonly _configurationService: IConfigurationService,109@IMarkdownRendererService private readonly _markdownRenderer: IMarkdownRendererService,110@IAccessibilityService private readonly _accessibilityService: IAccessibilityService111) {112super();113114this._linkHandler = options.linkHandler;115116this._target = 'targetElements' in options.target ? options.target : new ElementHoverTarget(options.target);117118if (options.style) {119switch (options.style) {120case HoverStyle.Pointer: {121options.appearance ??= {};122options.appearance.compact ??= true;123options.appearance.showPointer ??= true;124break;125}126case HoverStyle.Mouse: {127options.appearance ??= {};128options.appearance.compact ??= true;129break;130}131}132}133134this._hoverPointer = options.appearance?.showPointer ? $('div.workbench-hover-pointer') : undefined;135this._hover = this._register(new BaseHoverWidget(!options.appearance?.skipFadeInAnimation));136this._hover.containerDomNode.classList.add('workbench-hover');137if (options.appearance?.compact) {138this._hover.containerDomNode.classList.add('workbench-hover', 'compact');139}140if (options.additionalClasses) {141this._hover.containerDomNode.classList.add(...options.additionalClasses);142}143if (options.position?.forcePosition) {144this._forcePosition = true;145}146if (options.trapFocus) {147this._enableFocusTraps = true;148}149150const maxHeightRatio = options.appearance?.maxHeightRatio;151if (maxHeightRatio !== undefined && maxHeightRatio > 0 && maxHeightRatio <= 1) {152this._maxHeightRatioRelativeToWindow = maxHeightRatio;153}154155// Default to position above when the position is unspecified or a mouse event156this._hoverPosition = options.position?.hoverPosition === undefined157? HoverPosition.ABOVE158: isNumber(options.position.hoverPosition)159? options.position.hoverPosition160: HoverPosition.BELOW;161162// Don't allow mousedown out of the widget, otherwise preventDefault will call and text will163// not be selected.164this.onmousedown(this._hover.containerDomNode, e => e.stopPropagation());165166// Hide hover on escape167this.onkeydown(this._hover.containerDomNode, e => {168if (e.equals(KeyCode.Escape)) {169this.dispose();170}171});172173// Hide when the window loses focus174this._register(dom.addDisposableListener(this._targetWindow, 'blur', () => this.dispose()));175176const rowElement = $('div.hover-row.markdown-hover');177const contentsElement = $('div.hover-contents');178if (typeof options.content === 'string') {179contentsElement.textContent = options.content;180contentsElement.style.whiteSpace = 'pre-wrap';181182} else if (dom.isHTMLElement(options.content)) {183contentsElement.appendChild(options.content);184contentsElement.classList.add('html-hover-contents');185186} else {187const markdown = options.content;188189const { element } = this._register(this._markdownRenderer.render(markdown, {190actionHandler: this._linkHandler,191asyncRenderCallback: () => {192contentsElement.classList.add('code-hover-contents');193this.layout();194// This changes the dimensions of the hover so trigger a layout195this._onRequestLayout.fire();196}197}));198contentsElement.appendChild(element);199}200rowElement.appendChild(contentsElement);201this._hover.contentsDomNode.appendChild(rowElement);202203if (options.actions && options.actions.length > 0) {204const statusBarElement = $('div.hover-row.status-bar');205const actionsElement = $('div.actions');206options.actions.forEach(action => {207const keybinding = this._keybindingService.lookupKeybinding(action.commandId);208const keybindingLabel = keybinding ? keybinding.getLabel() : null;209this._register(HoverAction.render(actionsElement, {210label: action.label,211commandId: action.commandId,212run: e => {213action.run(e);214this.dispose();215},216iconClass: action.iconClass217}, keybindingLabel));218});219statusBarElement.appendChild(actionsElement);220this._hover.containerDomNode.appendChild(statusBarElement);221}222223this._hoverContainer = $('div.workbench-hover-container');224if (this._hoverPointer) {225this._hoverContainer.appendChild(this._hoverPointer);226}227this._hoverContainer.appendChild(this._hover.containerDomNode);228229// Determine whether to hide on hover230let hideOnHover: boolean;231if (options.actions && options.actions.length > 0) {232// If there are actions, require hover so they can be accessed233hideOnHover = false;234} else {235if (options.persistence?.hideOnHover === undefined) {236// When unset, will default to true when it's a string or when it's markdown that237// appears to have a link using a naive check for '](' and '</a>'238hideOnHover = typeof options.content === 'string' ||239isMarkdownString(options.content) && !options.content.value.includes('](') && !options.content.value.includes('</a>');240} else {241// It's set explicitly242hideOnHover = options.persistence.hideOnHover;243}244}245246// Show the hover hint if needed247if (options.appearance?.showHoverHint) {248const statusBarElement = $('div.hover-row.status-bar');249const infoElement = $('div.info');250infoElement.textContent = localize('hoverhint', 'Hold {0} key to mouse over', isMacintosh ? 'Option' : 'Alt');251statusBarElement.appendChild(infoElement);252this._hover.containerDomNode.appendChild(statusBarElement);253}254255const mouseTrackerTargets = [...this._target.targetElements];256if (!hideOnHover) {257mouseTrackerTargets.push(this._hoverContainer);258}259const mouseTracker = this._register(new CompositeMouseTracker(mouseTrackerTargets));260this._register(mouseTracker.onMouseOut(() => {261if (!this._isLocked) {262this.dispose();263}264}));265266// Setup another mouse tracker when hideOnHover is set in order to track the hover as well267// when it is locked. This ensures the hover will hide on mouseout after alt has been268// released to unlock the element.269if (hideOnHover) {270const mouseTracker2Targets = [...this._target.targetElements, this._hoverContainer];271this._lockMouseTracker = this._register(new CompositeMouseTracker(mouseTracker2Targets));272this._register(this._lockMouseTracker.onMouseOut(() => {273if (!this._isLocked) {274this.dispose();275}276}));277} else {278this._lockMouseTracker = mouseTracker;279}280}281282private addFocusTrap() {283if (!this._enableFocusTraps || this._addedFocusTrap) {284return;285}286this._addedFocusTrap = true;287288// Add a hover tab loop if the hover has at least one element with a valid tabIndex289const firstContainerFocusElement = this._hover.containerDomNode;290const lastContainerFocusElement = this.findLastFocusableChild(this._hover.containerDomNode);291if (lastContainerFocusElement) {292const beforeContainerFocusElement = dom.prepend(this._hoverContainer, $('div'));293const afterContainerFocusElement = dom.append(this._hoverContainer, $('div'));294beforeContainerFocusElement.tabIndex = 0;295afterContainerFocusElement.tabIndex = 0;296this._register(dom.addDisposableListener(afterContainerFocusElement, 'focus', (e) => {297firstContainerFocusElement.focus();298e.preventDefault();299}));300this._register(dom.addDisposableListener(beforeContainerFocusElement, 'focus', (e) => {301lastContainerFocusElement.focus();302e.preventDefault();303}));304}305}306307private findLastFocusableChild(root: Node): HTMLElement | undefined {308if (root.hasChildNodes()) {309for (let i = 0; i < root.childNodes.length; i++) {310const node = root.childNodes.item(root.childNodes.length - i - 1);311if (node.nodeType === node.ELEMENT_NODE) {312const parsedNode = node as HTMLElement;313if (typeof parsedNode.tabIndex === 'number' && parsedNode.tabIndex >= 0) {314return parsedNode;315}316}317const recursivelyFoundElement = this.findLastFocusableChild(node);318if (recursivelyFoundElement) {319return recursivelyFoundElement;320}321}322}323return undefined;324}325326public render(container: HTMLElement): void {327container.appendChild(this._hoverContainer);328const hoverFocused = this._hoverContainer.contains(this._hoverContainer.ownerDocument.activeElement);329const accessibleViewHint = hoverFocused && getHoverAccessibleViewHint(this._configurationService.getValue('accessibility.verbosity.hover') === true && this._accessibilityService.isScreenReaderOptimized(), this._keybindingService.lookupKeybinding('editor.action.accessibleView')?.getAriaLabel());330if (accessibleViewHint) {331332status(accessibleViewHint);333}334this.layout();335this.addFocusTrap();336}337338public layout() {339this._hover.containerDomNode.classList.remove('right-aligned');340this._hover.contentsDomNode.style.maxHeight = '';341342const getZoomAccountedBoundingClientRect = (e: HTMLElement) => {343const zoom = dom.getDomNodeZoomLevel(e);344345const boundingRect = e.getBoundingClientRect();346return {347top: boundingRect.top * zoom,348bottom: boundingRect.bottom * zoom,349right: boundingRect.right * zoom,350left: boundingRect.left * zoom,351};352};353354const targetBounds = this._target.targetElements.map(e => getZoomAccountedBoundingClientRect(e));355const { top, right, bottom, left } = targetBounds[0];356const width = right - left;357const height = bottom - top;358359const targetRect: TargetRect = {360top, right, bottom, left, width, height,361center: {362x: left + (width / 2),363y: top + (height / 2)364}365};366367// These calls adjust the position depending on spacing.368this.adjustHorizontalHoverPosition(targetRect);369this.adjustVerticalHoverPosition(targetRect);370// This call limits the maximum height of the hover.371this.adjustHoverMaxHeight(targetRect);372373// Offset the hover position if there is a pointer so it aligns with the target element374this._hoverContainer.style.padding = '';375this._hoverContainer.style.margin = '';376if (this._hoverPointer) {377switch (this._hoverPosition) {378case HoverPosition.RIGHT:379targetRect.left += Constants.PointerSize;380targetRect.right += Constants.PointerSize;381this._hoverContainer.style.paddingLeft = `${Constants.PointerSize}px`;382this._hoverContainer.style.marginLeft = `${-Constants.PointerSize}px`;383break;384case HoverPosition.LEFT:385targetRect.left -= Constants.PointerSize;386targetRect.right -= Constants.PointerSize;387this._hoverContainer.style.paddingRight = `${Constants.PointerSize}px`;388this._hoverContainer.style.marginRight = `${-Constants.PointerSize}px`;389break;390case HoverPosition.BELOW:391targetRect.top += Constants.PointerSize;392targetRect.bottom += Constants.PointerSize;393this._hoverContainer.style.paddingTop = `${Constants.PointerSize}px`;394this._hoverContainer.style.marginTop = `${-Constants.PointerSize}px`;395break;396case HoverPosition.ABOVE:397targetRect.top -= Constants.PointerSize;398targetRect.bottom -= Constants.PointerSize;399this._hoverContainer.style.paddingBottom = `${Constants.PointerSize}px`;400this._hoverContainer.style.marginBottom = `${-Constants.PointerSize}px`;401break;402}403404targetRect.center.x = targetRect.left + (width / 2);405targetRect.center.y = targetRect.top + (height / 2);406}407408this.computeXCordinate(targetRect);409this.computeYCordinate(targetRect);410411if (this._hoverPointer) {412// reset413this._hoverPointer.classList.remove('top');414this._hoverPointer.classList.remove('left');415this._hoverPointer.classList.remove('right');416this._hoverPointer.classList.remove('bottom');417418this.setHoverPointerPosition(targetRect);419}420this._hover.onContentsChanged();421}422423private computeXCordinate(target: TargetRect): void {424const hoverWidth = this._hover.containerDomNode.clientWidth + Constants.HoverBorderWidth;425426if (this._target.x !== undefined) {427this._x = this._target.x;428}429430else if (this._hoverPosition === HoverPosition.RIGHT) {431this._x = target.right;432}433434else if (this._hoverPosition === HoverPosition.LEFT) {435this._x = target.left - hoverWidth;436}437438else {439if (this._hoverPointer) {440this._x = target.center.x - (this._hover.containerDomNode.clientWidth / 2);441} else {442this._x = target.left;443}444445// Hover is going beyond window towards right end446if (this._x + hoverWidth >= this._targetDocumentElement.clientWidth) {447this._hover.containerDomNode.classList.add('right-aligned');448this._x = Math.max(this._targetDocumentElement.clientWidth - hoverWidth - Constants.HoverWindowEdgeMargin, this._targetDocumentElement.clientLeft);449}450}451452// Hover is going beyond window towards left end453if (this._x < this._targetDocumentElement.clientLeft) {454this._x = target.left + Constants.HoverWindowEdgeMargin;455}456457}458459private computeYCordinate(target: TargetRect): void {460if (this._target.y !== undefined) {461this._y = this._target.y;462}463464else if (this._hoverPosition === HoverPosition.ABOVE) {465this._y = target.top;466}467468else if (this._hoverPosition === HoverPosition.BELOW) {469this._y = target.bottom - 2;470}471472else {473if (this._hoverPointer) {474this._y = target.center.y + (this._hover.containerDomNode.clientHeight / 2);475} else {476this._y = target.bottom;477}478}479480// Hover on bottom is going beyond window481if (this._y > this._targetWindow.innerHeight) {482this._y = target.bottom;483}484}485486private adjustHorizontalHoverPosition(target: TargetRect): void {487// Do not adjust horizontal hover position if x cordiante is provided488if (this._target.x !== undefined) {489return;490}491492const hoverPointerOffset = (this._hoverPointer ? Constants.PointerSize : 0);493494// When force position is enabled, restrict max width495if (this._forcePosition) {496const padding = hoverPointerOffset + Constants.HoverBorderWidth;497if (this._hoverPosition === HoverPosition.RIGHT) {498this._hover.containerDomNode.style.maxWidth = `${this._targetDocumentElement.clientWidth - target.right - padding}px`;499} else if (this._hoverPosition === HoverPosition.LEFT) {500this._hover.containerDomNode.style.maxWidth = `${target.left - padding}px`;501}502return;503}504505// Position hover on right to target506if (this._hoverPosition === HoverPosition.RIGHT) {507const roomOnRight = this._targetDocumentElement.clientWidth - target.right;508// Hover on the right is going beyond window.509if (roomOnRight < this._hover.containerDomNode.clientWidth + hoverPointerOffset) {510const roomOnLeft = target.left;511// There's enough room on the left, flip the hover position512if (roomOnLeft >= this._hover.containerDomNode.clientWidth + hoverPointerOffset) {513this._hoverPosition = HoverPosition.LEFT;514}515// Hover on the left would go beyond window too516else {517this._hoverPosition = HoverPosition.BELOW;518}519}520}521// Position hover on left to target522else if (this._hoverPosition === HoverPosition.LEFT) {523524const roomOnLeft = target.left;525// Hover on the left is going beyond window.526if (roomOnLeft < this._hover.containerDomNode.clientWidth + hoverPointerOffset) {527const roomOnRight = this._targetDocumentElement.clientWidth - target.right;528// There's enough room on the right, flip the hover position529if (roomOnRight >= this._hover.containerDomNode.clientWidth + hoverPointerOffset) {530this._hoverPosition = HoverPosition.RIGHT;531}532// Hover on the right would go beyond window too533else {534this._hoverPosition = HoverPosition.BELOW;535}536}537// Hover on the left is going beyond window.538if (target.left - this._hover.containerDomNode.clientWidth - hoverPointerOffset <= this._targetDocumentElement.clientLeft) {539this._hoverPosition = HoverPosition.RIGHT;540}541}542}543544private adjustVerticalHoverPosition(target: TargetRect): void {545// Do not adjust vertical hover position if the y coordinate is provided546// or the position is forced547if (this._target.y !== undefined || this._forcePosition) {548return;549}550551const hoverPointerOffset = (this._hoverPointer ? Constants.PointerSize : 0);552553// Position hover on top of the target554if (this._hoverPosition === HoverPosition.ABOVE) {555// Hover on top is going beyond window556if (target.top - this._hover.containerDomNode.clientHeight - hoverPointerOffset < 0) {557this._hoverPosition = HoverPosition.BELOW;558}559}560561// Position hover below the target562else if (this._hoverPosition === HoverPosition.BELOW) {563// Hover on bottom is going beyond window564if (target.bottom + this._hover.containerDomNode.offsetHeight + hoverPointerOffset > this._targetWindow.innerHeight) {565this._hoverPosition = HoverPosition.ABOVE;566}567}568}569570private adjustHoverMaxHeight(target: TargetRect): void {571let maxHeight = this._targetWindow.innerHeight * this._maxHeightRatioRelativeToWindow;572573// When force position is enabled, restrict max height574if (this._forcePosition) {575const padding = (this._hoverPointer ? Constants.PointerSize : 0) + Constants.HoverBorderWidth;576if (this._hoverPosition === HoverPosition.ABOVE) {577maxHeight = Math.min(maxHeight, target.top - padding);578} else if (this._hoverPosition === HoverPosition.BELOW) {579maxHeight = Math.min(maxHeight, this._targetWindow.innerHeight - target.bottom - padding);580}581}582583this._hover.containerDomNode.style.maxHeight = `${maxHeight}px`;584if (this._hover.contentsDomNode.clientHeight < this._hover.contentsDomNode.scrollHeight) {585// Add padding for a vertical scrollbar586const extraRightPadding = `${this._hover.scrollbar.options.verticalScrollbarSize}px`;587if (this._hover.contentsDomNode.style.paddingRight !== extraRightPadding) {588this._hover.contentsDomNode.style.paddingRight = extraRightPadding;589}590}591}592593private setHoverPointerPosition(target: TargetRect): void {594if (!this._hoverPointer) {595return;596}597598switch (this._hoverPosition) {599case HoverPosition.LEFT:600case HoverPosition.RIGHT: {601this._hoverPointer.classList.add(this._hoverPosition === HoverPosition.LEFT ? 'right' : 'left');602const hoverHeight = this._hover.containerDomNode.clientHeight;603604// If hover is taller than target, then show the pointer at the center of target605if (hoverHeight > target.height) {606this._hoverPointer.style.top = `${target.center.y - (this._y - hoverHeight) - Constants.PointerSize}px`;607}608609// Otherwise show the pointer at the center of hover610else {611this._hoverPointer.style.top = `${Math.round((hoverHeight / 2)) - Constants.PointerSize}px`;612}613614break;615}616case HoverPosition.ABOVE:617case HoverPosition.BELOW: {618this._hoverPointer.classList.add(this._hoverPosition === HoverPosition.ABOVE ? 'bottom' : 'top');619const hoverWidth = this._hover.containerDomNode.clientWidth;620621// Position pointer at the center of the hover622let pointerLeftPosition = Math.round((hoverWidth / 2)) - Constants.PointerSize;623624// If pointer goes beyond target then position it at the center of the target625const pointerX = this._x + pointerLeftPosition;626if (pointerX < target.left || pointerX > target.right) {627pointerLeftPosition = target.center.x - this._x - Constants.PointerSize;628}629630this._hoverPointer.style.left = `${pointerLeftPosition}px`;631break;632}633}634}635636public focus() {637this._hover.containerDomNode.focus();638}639640public hide(): void {641this.dispose();642}643644public override dispose(): void {645if (!this._isDisposed) {646this._onDispose.fire();647this._target.dispose?.();648this._hoverContainer.remove();649this._messageListeners.dispose();650super.dispose();651}652this._isDisposed = true;653}654}655656class CompositeMouseTracker extends Widget {657private _isMouseIn: boolean = true;658private readonly _mouseTimer: MutableDisposable<TimeoutTimer> = this._register(new MutableDisposable());659660private readonly _onMouseOut = this._register(new Emitter<void>());661get onMouseOut(): Event<void> { return this._onMouseOut.event; }662663get isMouseIn(): boolean { return this._isMouseIn; }664665/**666* @param _elements The target elements to track mouse in/out events on.667* @param _eventDebounceDelay The delay in ms to debounce the event firing. This is used to668* allow a short period for the mouse to move into the hover or a nearby target element. For669* example hovering a scroll bar will not hide the hover immediately.670*/671constructor(672private _elements: HTMLElement[],673private _eventDebounceDelay: number = 200674) {675super();676677for (const element of this._elements) {678this.onmouseover(element, () => this._onTargetMouseOver());679this.onmouseleave(element, () => this._onTargetMouseLeave());680}681}682683private _onTargetMouseOver(): void {684this._isMouseIn = true;685this._mouseTimer.clear();686}687688private _onTargetMouseLeave(): void {689this._isMouseIn = false;690// Evaluate whether the mouse is still outside asynchronously such that other mouse targets691// have the opportunity to first their mouse in event.692this._mouseTimer.value = new TimeoutTimer(() => this._fireIfMouseOutside(), this._eventDebounceDelay);693}694695private _fireIfMouseOutside(): void {696if (!this._isMouseIn) {697this._onMouseOut.fire();698}699}700701/**702* Adds an element to be tracked by this mouse tracker. Mouse events on this703* element will be considered as being "inside" the tracked area.704*/705addElement(element: HTMLElement): IDisposable {706if (this._elements.includes(element)) {707return Disposable.None;708}709this._elements.push(element);710const store = new DisposableStore();711store.add(dom.addDisposableListener(element, dom.EventType.MOUSE_OVER, () => this._onTargetMouseOver()));712store.add(dom.addDisposableListener(element, dom.EventType.MOUSE_LEAVE, () => this._onTargetMouseLeave()));713store.add(toDisposable(() => {714const index = this._elements.indexOf(element);715if (index >= 0) {716this._elements.splice(index, 1);717}718}));719return store;720}721}722723class ElementHoverTarget implements IHoverTarget {724readonly targetElements: readonly HTMLElement[];725726constructor(727private _element: HTMLElement728) {729this.targetElements = [this._element];730}731732dispose(): void {733}734}735736737