diff --git a/addons/xterm-addon-webgl/src/renderLayer/LinkRenderLayer.ts b/addons/xterm-addon-webgl/src/renderLayer/LinkRenderLayer.ts index ebf773d3b..10cf9c1fe 100644 --- a/addons/xterm-addon-webgl/src/renderLayer/LinkRenderLayer.ts +++ b/addons/xterm-addon-webgl/src/renderLayer/LinkRenderLayer.ts @@ -16,11 +16,11 @@ export class LinkRenderLayer extends BaseRenderLayer { constructor(container: HTMLElement, zIndex: number, colors: IColorSet, terminal: ILinkifierAccessor) { super(container, 'link', zIndex, true, colors); - terminal.linkifier.onLinkHover(e => this._onLinkHover(e)); - terminal.linkifier.onLinkLeave(e => this._onLinkLeave(e)); + terminal.linkifier.onShowLinkUnderline(e => this._onShowLinkUnderline(e)); + terminal.linkifier.onHideLinkUnderline(e => this._onHideLinkUnderline(e)); - terminal.linkifier2.onLinkHover(e => this._onLinkHover(e)); - terminal.linkifier2.onLinkLeave(e => this._onLinkLeave(e)); + terminal.linkifier2.onShowLinkUnderline(e => this._onShowLinkUnderline(e)); + terminal.linkifier2.onHideLinkUnderline(e => this._onHideLinkUnderline(e)); } public resize(terminal: Terminal, dim: IRenderDimensions): void { @@ -45,7 +45,7 @@ export class LinkRenderLayer extends BaseRenderLayer { } } - private _onLinkHover(e: ILinkifierEvent): void { + private _onShowLinkUnderline(e: ILinkifierEvent): void { if (e.fg === INVERTED_DEFAULT_COLOR) { this._ctx.fillStyle = this._colors.background.css; } else if (e.fg !== undefined && is256Color(e.fg)) { @@ -69,7 +69,7 @@ export class LinkRenderLayer extends BaseRenderLayer { this._state = e; } - private _onLinkLeave(e: ILinkifierEvent): void { + private _onHideLinkUnderline(e: ILinkifierEvent): void { this._clearCurrentLink(); } } diff --git a/src/browser/Linkifier.ts b/src/browser/Linkifier.ts index 971501b70..ca45ef2a9 100644 --- a/src/browser/Linkifier.ts +++ b/src/browser/Linkifier.ts @@ -35,10 +35,10 @@ export class Linkifier implements ILinkifier { private _nextLinkMatcherId = 0; private _rowsToLinkify: { start: number | undefined, end: number | undefined }; - private _onLinkHover = new EventEmitter(); - public get onLinkHover(): IEvent { return this._onLinkHover.event; } - private _onLinkLeave = new EventEmitter(); - public get onLinkLeave(): IEvent { return this._onLinkLeave.event; } + private _onShowLinkUnderline = new EventEmitter(); + public get onShowLinkUnderline(): IEvent { return this._onShowLinkUnderline.event; } + private _onHideLinkUnderline = new EventEmitter(); + public get onHideLinkUnderline(): IEvent { return this._onHideLinkUnderline.event; } private _onLinkTooltip = new EventEmitter(); public get onLinkTooltip(): IEvent { return this._onLinkTooltip.event; } @@ -307,7 +307,7 @@ export class Linkifier implements ILinkifier { } }, () => { - this._onLinkHover.fire(this._createLinkHoverEvent(x1, y1, x2, y2, fg)); + this._onShowLinkUnderline.fire(this._createLinkHoverEvent(x1, y1, x2, y2, fg)); this._element!.classList.add('xterm-cursor-pointer'); }, e => { @@ -319,7 +319,7 @@ export class Linkifier implements ILinkifier { } }, () => { - this._onLinkLeave.fire(this._createLinkHoverEvent(x1, y1, x2, y2, fg)); + this._onHideLinkUnderline.fire(this._createLinkHoverEvent(x1, y1, x2, y2, fg)); this._element!.classList.remove('xterm-cursor-pointer'); if (matcher.hoverLeaveCallback) { matcher.hoverLeaveCallback(); diff --git a/src/browser/Linkifier2.test.ts b/src/browser/Linkifier2.test.ts index bcf068a84..36a6da431 100644 --- a/src/browser/Linkifier2.test.ts +++ b/src/browser/Linkifier2.test.ts @@ -48,7 +48,7 @@ describe('Linkifier2', () => { }; it('onLinkHover event range is correct', done => { - linkifier.onLinkHover(e => { + linkifier.onShowLinkUnderline(e => { assert.equal(link.range.start.x - 1, e.x1); assert.equal(link.range.start.y - 1, e.y1); assert.equal(link.range.end.x, e.x2); @@ -61,7 +61,7 @@ describe('Linkifier2', () => { }); it('onLinkLeave event range is correct', done => { - linkifier.onLinkLeave(e => { + linkifier.onHideLinkUnderline(e => { assert.equal(link.range.start.x - 1, e.x1); assert.equal(link.range.start.y - 1, e.y1); assert.equal(link.range.end.x, e.x2); diff --git a/src/browser/Linkifier2.ts b/src/browser/Linkifier2.ts index 128c21eec..3f45e6573 100644 --- a/src/browser/Linkifier2.ts +++ b/src/browser/Linkifier2.ts @@ -9,20 +9,26 @@ import { IMouseService, IRenderService } from './services/Services'; import { IBufferService } from 'common/services/Services'; import { EventEmitter, IEvent } from 'common/EventEmitter'; +interface ILinkState { + hideDecorations: boolean; + isHovered: boolean; +} + export class Linkifier2 implements ILinkifier2 { private _element: HTMLElement | undefined; private _mouseService: IMouseService | undefined; private _renderService: IRenderService | undefined; private _linkProviders: ILinkProvider[] = []; private _currentLink: ILink | undefined; + private _currentLinkState: ILinkState | undefined; private _lastMouseEvent: MouseEvent | undefined; private _linkCacheDisposables: IDisposable[] = []; private _lastBufferCell: IBufferCellPosition | undefined; - private _onLinkHover = new EventEmitter(); - public get onLinkHover(): IEvent { return this._onLinkHover.event; } - private _onLinkLeave = new EventEmitter(); - public get onLinkLeave(): IEvent { return this._onLinkLeave.event; } + private _onShowLinkUnderline = new EventEmitter(); + public get onShowLinkUnderline(): IEvent { return this._onShowLinkUnderline.event; } + private _onHideLinkUnderline = new EventEmitter(); + public get onHideLinkUnderline(): IEvent { return this._onHideLinkUnderline.event; } constructor( private readonly _bufferService: IBufferService @@ -67,6 +73,20 @@ export class Linkifier2 implements ILinkifier2 { return; } + // Ignore the event if it's an embedder created hover widget + const composedPath = event.composedPath() as HTMLElement[]; + for (let i = 0; i < composedPath.length; i++) { + const target = composedPath[i]; + // Hit Terminal.element, break and continue + if (target.classList.contains('xterm')) { + break; + } + // It's a hover, don't respect hover event + if (target.classList.contains('xterm-hover')) { + return; + } + } + if (!this._lastBufferCell || (position.x !== this._lastBufferCell.x || position.y !== this._lastBufferCell.y)) { this._onHover(position); this._lastBufferCell = position; @@ -151,6 +171,7 @@ export class Linkifier2 implements ILinkifier2 { if (!startRow || !endRow || (this._currentLink.range.start.y >= startRow && this._currentLink.range.end.y <= endRow)) { this._linkLeave(this._element, this._currentLink, this._lastMouseEvent); this._currentLink = undefined; + this._currentLinkState = undefined; this._linkCacheDisposables.forEach(l => l.dispose()); this._linkCacheDisposables = []; } @@ -170,8 +191,28 @@ export class Linkifier2 implements ILinkifier2 { // Trigger hover if the we have a link at the position if (this._linkAtPosition(link, position)) { this._currentLink = link; + this._currentLinkState = { + hideDecorations: link.hideDecorations || false, + isHovered: true + }; this._linkHover(this._element, link, this._lastMouseEvent); + // Add listener for tracking hideDecorations changes + Object.defineProperties(link, { + hideDecorations: { + get: () => this._currentLinkState?.hideDecorations, + set: v => { + if (this._currentLinkState && this._currentLinkState.hideDecorations !== v) { + this._currentLinkState.hideDecorations = v; + if (this._currentLinkState?.isHovered) { + this._fireUnderlineEvent(link, !v); + this._element?.classList.toggle('xterm-cursor-pointer', !v); + } + } + } + } + }); + // Add listener for rerendering if (this._renderService) { this._linkCacheDisposables.push(this._renderService.onRenderedBufferChange(e => { @@ -182,25 +223,35 @@ export class Linkifier2 implements ILinkifier2 { } protected _linkHover(element: HTMLElement, link: ILink, event: MouseEvent): void { - const range = link.range; - const scrollOffset = this._bufferService.buffer.ydisp; - - if (!link.hideUnderline) { - this._onLinkHover.fire(this._createLinkHoverEvent(range.start.x - 1, range.start.y - scrollOffset - 1, range.end.x, range.end.y - scrollOffset - 1, undefined)); + if (!link.hideDecorations) { + this._fireUnderlineEvent(link, true); + element.classList.add('xterm-cursor-pointer'); + } + if (this._currentLinkState) { + this._currentLinkState.isHovered = true; } - element.classList.add('xterm-cursor-pointer'); if (link.hover) { link.hover(event, link.text); } } - protected _linkLeave(element: HTMLElement, link: ILink, event: MouseEvent): void { + private _fireUnderlineEvent(link: ILink, showEvent: boolean): void { const range = link.range; const scrollOffset = this._bufferService.buffer.ydisp; + const event = this._createLinkUnderlineEvent(range.start.x - 1, range.start.y - scrollOffset - 1, range.end.x, range.end.y - scrollOffset - 1, undefined); + const emitter = showEvent ? this._onShowLinkUnderline : this._onHideLinkUnderline; + emitter.fire(event); + } - this._onLinkLeave.fire(this._createLinkHoverEvent(range.start.x - 1, range.start.y - scrollOffset - 1, range.end.x, range.end.y - scrollOffset - 1, undefined)); - element.classList.remove('xterm-cursor-pointer'); + protected _linkLeave(element: HTMLElement, link: ILink, event: MouseEvent): void { + if (!link.hideDecorations) { + this._fireUnderlineEvent(link, false); + element.classList.remove('xterm-cursor-pointer'); + } + if (this._currentLinkState) { + this._currentLinkState.isHovered = false; + } if (link.leave) { link.leave(event, link.text); @@ -240,7 +291,7 @@ export class Linkifier2 implements ILinkifier2 { return { x: coords[0], y: coords[1] + this._bufferService.buffer.ydisp }; } - private _createLinkHoverEvent(x1: number, y1: number, x2: number, y2: number, fg: number | undefined): ILinkifierEvent { + private _createLinkUnderlineEvent(x1: number, y1: number, x2: number, y2: number, fg: number | undefined): ILinkifierEvent { return { x1, y1, x2, y2, cols: this._bufferService.cols, fg }; } } diff --git a/src/browser/Types.d.ts b/src/browser/Types.d.ts index fc790a7ec..382d1c429 100644 --- a/src/browser/Types.d.ts +++ b/src/browser/Types.d.ts @@ -96,8 +96,8 @@ export interface ILinkifierEvent { } export interface ILinkifier { - onLinkHover: IEvent; - onLinkLeave: IEvent; + onShowLinkUnderline: IEvent; + onHideLinkUnderline: IEvent; onLinkTooltip: IEvent; attachToDom(element: HTMLElement, mouseZoneManager: IMouseZoneManager): void; @@ -107,8 +107,8 @@ export interface ILinkifier { } export interface ILinkifier2 { - onLinkHover: IEvent; - onLinkLeave: IEvent; + onShowLinkUnderline: IEvent; + onHideLinkUnderline: IEvent; attachToDom(element: HTMLElement, mouseService: IMouseService, renderService: IRenderService): void; registerLinkProvider(linkProvider: ILinkProvider): IDisposable; @@ -172,7 +172,7 @@ interface ILinkProvider { interface ILink { range: IBufferRange; text: string; - hideUnderline?: boolean; + hideDecorations?: boolean; activate(event: MouseEvent, text: string): void; hover?(event: MouseEvent, text: string): void; leave?(event: MouseEvent, text: string): void; diff --git a/src/browser/renderer/LinkRenderLayer.ts b/src/browser/renderer/LinkRenderLayer.ts index 33f80cdc3..c41955d93 100644 --- a/src/browser/renderer/LinkRenderLayer.ts +++ b/src/browser/renderer/LinkRenderLayer.ts @@ -24,11 +24,11 @@ export class LinkRenderLayer extends BaseRenderLayer { optionsService: IOptionsService ) { super(container, 'link', zIndex, true, colors, rendererId, bufferService, optionsService); - linkifier.onLinkHover(e => this._onLinkHover(e)); - linkifier.onLinkLeave(e => this._onLinkLeave(e)); + linkifier.onShowLinkUnderline(e => this._onShowLinkUnderline(e)); + linkifier.onHideLinkUnderline(e => this._onHideLinkUnderline(e)); - linkifier2.onLinkHover(e => this._onLinkHover(e)); - linkifier2.onLinkLeave(e => this._onLinkLeave(e)); + linkifier2.onShowLinkUnderline(e => this._onShowLinkUnderline(e)); + linkifier2.onHideLinkUnderline(e => this._onHideLinkUnderline(e)); } public resize(dim: IRenderDimensions): void { @@ -53,7 +53,7 @@ export class LinkRenderLayer extends BaseRenderLayer { } } - private _onLinkHover(e: ILinkifierEvent): void { + private _onShowLinkUnderline(e: ILinkifierEvent): void { if (e.fg === INVERTED_DEFAULT_COLOR) { this._ctx.fillStyle = this._colors.background.css; } else if (e.fg && is256Color(e.fg)) { @@ -77,7 +77,7 @@ export class LinkRenderLayer extends BaseRenderLayer { this._state = e; } - private _onLinkLeave(e: ILinkifierEvent): void { + private _onHideLinkUnderline(e: ILinkifierEvent): void { this._clearCurrentLink(); } } diff --git a/src/browser/renderer/dom/DomRenderer.ts b/src/browser/renderer/dom/DomRenderer.ts index a2e370fc4..1303ec1b5 100644 --- a/src/browser/renderer/dom/DomRenderer.ts +++ b/src/browser/renderer/dom/DomRenderer.ts @@ -86,11 +86,11 @@ export class DomRenderer extends Disposable implements IRenderer { this._screenElement.appendChild(this._rowContainer); this._screenElement.appendChild(this._selectionContainer); - this._linkifier.onLinkHover(e => this._onLinkHover(e)); - this._linkifier.onLinkLeave(e => this._onLinkLeave(e)); + this._linkifier.onShowLinkUnderline(e => this._onLinkHover(e)); + this._linkifier.onHideLinkUnderline(e => this._onLinkLeave(e)); - this._linkifier2.onLinkHover(e => this._onLinkHover(e)); - this._linkifier2.onLinkLeave(e => this._onLinkLeave(e)); + this._linkifier2.onShowLinkUnderline(e => this._onLinkHover(e)); + this._linkifier2.onHideLinkUnderline(e => this._onLinkLeave(e)); } public dispose(): void { diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 9830f4d9f..c89011766 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -1121,9 +1121,10 @@ declare module 'xterm' { text: string; /** - * Whether to hide the link's underline. + * Whether to hide the underline and cursor styles, this property is tracked and changes made + * after the link is provided will trigger changes. */ - hideUnderline?: boolean; + hideDecorations?: boolean; /** * Calls when the link is activated. @@ -1133,7 +1134,9 @@ declare module 'xterm' { activate(event: MouseEvent, text: string): void; /** - * Called when the mouse hovers the link. + * Called when the mouse hovers the link. To use this to create a DOM-based hover tooltip, + * create the hover element within `Terminal.element` and add the `xterm-hover` class to it, + * that will cause mouse events to not fall through and activate other links. * @param event The mouse event triggering the callback. * @param text The text of the link. */