From 8709d85833f2fde27bea7a396d5e2ffda0340c8a Mon Sep 17 00:00:00 2001 From: Changhao Han Date: Wed, 29 Sep 2021 23:15:32 +0000 Subject: [PATCH] Enable isolation mode for container query containers screencast: https://imgur.com/a/61Tvewc backend CL: https://crrev.com/c/3173212 Bug: chromium:1146422 Change-Id: I57abf59ae8d22927c3fb514e4dd684d5656b410a Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/3175289 Reviewed-by: Alex Rudenko Reviewed-by: Johan Bay Commit-Queue: Changhao Han --- front_end/core/common/Color.ts | 6 + front_end/core/i18n/locales/en-US.json | 6 + front_end/core/i18n/locales/en-XL.json | 6 + front_end/core/sdk/OverlayModel.ts | 21 +++ .../core/sdk/OverlayPersistentHighlighter.ts | 58 ++++++- .../panels/elements/ElementsTreeElement.ts | 19 +++ inspector_overlay/BUILD.gn | 6 +- .../tool_persistent_isolated_element.html | 75 +++++++++ inspector_overlay/drag_resize_handler.ts | 142 ++++++++++++++++++ .../highlight_isolated_element.ts | 62 ++++++++ inspector_overlay/main.ts | 4 +- inspector_overlay/tool_highlight.ts | 6 + inspector_overlay/tool_paused.ts | 2 + inspector_overlay/tool_persistent.ts | 110 ++++++++++++++ test/e2e/elements/BUILD.gn | 1 + test/e2e/elements/isolation-mode_test.ts | 15 ++ .../sdk/OverlayPersistentHighlighter_test.ts | 13 ++ 17 files changed, 547 insertions(+), 5 deletions(-) create mode 100644 inspector_overlay/debug/tool_persistent_isolated_element.html create mode 100644 inspector_overlay/drag_resize_handler.ts create mode 100644 inspector_overlay/highlight_isolated_element.ts create mode 100644 test/e2e/elements/isolation-mode_test.ts diff --git a/front_end/core/common/Color.ts b/front_end/core/common/Color.ts index dea99bb635..a8d2e622cf 100644 --- a/front_end/core/common/Color.ts +++ b/front_end/core/common/Color.ts @@ -875,6 +875,12 @@ export const SourceOrderHighlight = { ChildOutline: Color.fromRGBA([0, 120, 212, 1]), }; +export const IsolationModeHighlight = { + Resizer: Color.fromRGBA([222, 225, 230, 1]), // --color-background-elevation-2 + ResizerHandle: Color.fromRGBA([166, 166, 166, 1]), + Mask: Color.fromRGBA([248, 249, 249, 1]), +}; + export class Generator { private readonly hueSpace: number|{ min: number, diff --git a/front_end/core/i18n/locales/en-US.json b/front_end/core/i18n/locales/en-US.json index d8d6e6b506..6f3a905d77 100644 --- a/front_end/core/i18n/locales/en-US.json +++ b/front_end/core/i18n/locales/en-US.json @@ -4235,6 +4235,12 @@ "panels/elements/ElementsTreeElement.ts | enableScrollSnap": { "message": "Enable scroll-snap overlay" }, + "panels/elements/ElementsTreeElement.ts | enterIsolationMode": { + "message": "Enter Isolation Mode" + }, + "panels/elements/ElementsTreeElement.ts | exitIsolationMode": { + "message": "Exit Isolation Mode" + }, "panels/elements/ElementsTreeElement.ts | expandRecursively": { "message": "Expand recursively" }, diff --git a/front_end/core/i18n/locales/en-XL.json b/front_end/core/i18n/locales/en-XL.json index bc1743ddd0..9c4d3bebe6 100644 --- a/front_end/core/i18n/locales/en-XL.json +++ b/front_end/core/i18n/locales/en-XL.json @@ -4235,6 +4235,12 @@ "panels/elements/ElementsTreeElement.ts | enableScrollSnap": { "message": "Êńâb́l̂é ŝćr̂ól̂ĺ-ŝńâṕ ôv́êŕl̂áŷ" }, + "panels/elements/ElementsTreeElement.ts | enterIsolationMode": { + "message": "Êńt̂ér̂ Íŝól̂át̂íôń M̂ód̂é" + }, + "panels/elements/ElementsTreeElement.ts | exitIsolationMode": { + "message": "Êx́ît́ Îśôĺât́îón̂ Ḿôd́ê" + }, "panels/elements/ElementsTreeElement.ts | expandRecursively": { "message": "Êx́p̂án̂d́ r̂éĉúr̂śîv́êĺŷ" }, diff --git a/front_end/core/sdk/OverlayModel.ts b/front_end/core/sdk/OverlayModel.ts index c9f84b546c..04c974b5bc 100644 --- a/front_end/core/sdk/OverlayModel.ts +++ b/front_end/core/sdk/OverlayModel.ts @@ -467,6 +467,27 @@ export class OverlayModel extends SDKModel implements ProtocolProxyA return this.sourceOrderModeActiveInternal; } + highlightIsolatedElementInPersistentOverlay(nodeId: Protocol.DOM.NodeId): void { + if (!this.persistentHighlighter) { + return; + } + this.persistentHighlighter.highlightIsolatedElementInOverlay(nodeId); + } + + hideIsolatedElementInPersistentOverlay(nodeId: Protocol.DOM.NodeId): void { + if (!this.persistentHighlighter) { + return; + } + this.persistentHighlighter.hideIsolatedElementInOverlay(nodeId); + } + + isHighlightedIsolatedElementInPersistentOverlay(nodeId: Protocol.DOM.NodeId): boolean { + if (!this.persistentHighlighter) { + return false; + } + return this.persistentHighlighter.isIsolatedElementHighlighted(nodeId); + } + private delayedHideHighlight(delay: number): void { if (this.hideHighlightTimeout === null) { this.hideHighlightTimeout = window.setTimeout(() => this.highlightInOverlay({clear: true}), delay); diff --git a/front_end/core/sdk/OverlayPersistentHighlighter.ts b/front_end/core/sdk/OverlayPersistentHighlighter.ts index a715005771..5a0cbf729e 100644 --- a/front_end/core/sdk/OverlayPersistentHighlighter.ts +++ b/front_end/core/sdk/OverlayPersistentHighlighter.ts @@ -14,6 +14,7 @@ export class OverlayPersistentHighlighter { private readonly flexHighlights: Map; private readonly containerQueryHighlights: Map; + private readonly isolatedElementHighlights: Map; private readonly colors: Map; private gridColorGenerator: OverlayColorGenerator; private flexColorGenerator: OverlayColorGenerator; @@ -41,6 +42,8 @@ export class OverlayPersistentHighlighter { this.containerQueryHighlights = new Map(); + this.isolatedElementHighlights = new Map(); + this.colors = new Map(); this.gridColorGenerator = new OverlayColorGenerator(); @@ -223,11 +226,36 @@ export class OverlayPersistentHighlighter { }; } + highlightIsolatedElementInOverlay(nodeId: Protocol.DOM.NodeId): void { + this.isolatedElementHighlights.set(nodeId, this.buildIsolationModeHighlightConfig()); + this.updateHighlightsInOverlay(); + } + + hideIsolatedElementInOverlay(nodeId: Protocol.DOM.NodeId): void { + if (this.isolatedElementHighlights.has(nodeId)) { + this.isolatedElementHighlights.delete(nodeId); + this.updateHighlightsInOverlay(); + } + } + + isIsolatedElementHighlighted(nodeId: Protocol.DOM.NodeId): boolean { + return this.isolatedElementHighlights.has(nodeId); + } + + private buildIsolationModeHighlightConfig(): Protocol.Overlay.IsolationModeHighlightConfig { + return { + resizerColor: Common.Color.IsolationModeHighlight.Resizer.toProtocolRGBA(), + resizerHandleColor: Common.Color.IsolationModeHighlight.ResizerHandle.toProtocolRGBA(), + maskColor: Common.Color.IsolationModeHighlight.Mask.toProtocolRGBA(), + }; + } + hideAllInOverlay(): void { this.flexHighlights.clear(); this.gridHighlights.clear(); this.scrollSnapHighlights.clear(); this.containerQueryHighlights.clear(); + this.isolatedElementHighlights.clear(); this.updateHighlightsInOverlay(); } @@ -236,7 +264,9 @@ export class OverlayPersistentHighlighter { const flexboxesNeedUpdate = this.updateHighlightsForDeletedNodes(this.flexHighlights); const scrollSnapsNeedUpdate = this.updateHighlightsForDeletedNodes(this.scrollSnapHighlights); const containerQueriesNeedUpdate = this.updateHighlightsForDeletedNodes(this.containerQueryHighlights); - if (flexboxesNeedUpdate || gridsNeedUpdate || scrollSnapsNeedUpdate || containerQueriesNeedUpdate) { + const isolatedElementsNeedUpdate = this.updateHighlightsForDeletedNodes(this.isolatedElementHighlights); + if (flexboxesNeedUpdate || gridsNeedUpdate || scrollSnapsNeedUpdate || containerQueriesNeedUpdate || + isolatedElementsNeedUpdate) { this.updateHighlightsInOverlay(); } } @@ -265,17 +295,21 @@ export class OverlayPersistentHighlighter { for (const nodeId of this.containerQueryHighlights.keys()) { this.containerQueryHighlights.set(nodeId, this.buildContainerQueryContainerHighlightConfig()); } + for (const nodeId of this.isolatedElementHighlights.keys()) { + this.isolatedElementHighlights.set(nodeId, this.buildIsolationModeHighlightConfig()); + } this.updateHighlightsInOverlay(); } private updateHighlightsInOverlay(): void { - const hasNodesToHighlight = - this.gridHighlights.size > 0 || this.flexHighlights.size > 0 || this.containerQueryHighlights.size > 0; + const hasNodesToHighlight = this.gridHighlights.size > 0 || this.flexHighlights.size > 0 || + this.containerQueryHighlights.size > 0 || this.isolatedElementHighlights.size > 0; this.model.setShowViewportSizeOnResize(!hasNodesToHighlight); this.updateGridHighlightsInOverlay(); this.updateFlexHighlightsInOverlay(); this.updateScrollSnapHighlightsInOverlay(); this.updateContainerQueryHighlightsInOverlay(); + this.updateIsolatedElementHighlightsInOverlay(); } private updateGridHighlightsInOverlay(): void { @@ -316,6 +350,15 @@ export class OverlayPersistentHighlighter { } overlayModel.target().overlayAgent().invoke_setShowContainerQueryOverlays({containerQueryHighlightConfigs}); } + + updateIsolatedElementHighlightsInOverlay(): void { + const overlayModel = this.model; + const isolatedElementHighlightConfigs = []; + for (const [nodeId, isolationModeHighlightConfig] of this.isolatedElementHighlights.entries()) { + isolatedElementHighlightConfigs.push({nodeId, isolationModeHighlightConfig}); + } + overlayModel.target().overlayAgent().invoke_setShowIsolatedElements({isolatedElementHighlightConfigs}); + } } /** @@ -362,6 +405,15 @@ export interface OverlayAgent { containerQueryContainerHighlightConfig: Protocol.Overlay.ContainerQueryContainerHighlightConfig, }>, }): void; + + // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration + // eslint-disable-next-line @typescript-eslint/naming-convention + invoke_setShowIsolatedElements(param: { + isolatedElementHighlightConfigs: Array<{ + nodeId: number, + isolationModeHighlightConfig: Protocol.Overlay.IsolationModeHighlightConfig, + }>, + }): void; } /** diff --git a/front_end/panels/elements/ElementsTreeElement.ts b/front_end/panels/elements/ElementsTreeElement.ts index 4dcd67340d..0ccc4893e1 100644 --- a/front_end/panels/elements/ElementsTreeElement.ts +++ b/front_end/panels/elements/ElementsTreeElement.ts @@ -83,6 +83,14 @@ const UIStrings = { */ scrollIntoView: 'Scroll into view', /** + *@description Text to enter Isolation Mode, a mode with focus on a single element and interactive resizing + */ + enterIsolationMode: 'Enter Isolation Mode', + /** + *@description Text to exit Isolation Mode, a mode with focus on a single element and interactive resizing + */ + exitIsolationMode: 'Exit Isolation Mode', + /** *@description A context menu item in the Elements Tree Element of the Elements panel */ editText: 'Edit text', @@ -636,6 +644,17 @@ export class ElementsTreeElement extends UI.TreeOutline.TreeElement { contextMenu.viewSection().appendItem(i18nString(UIStrings.focus), async () => { await this.nodeInternal.focus(); }); + + const overlayModel = this.nodeInternal.domModel().overlayModel(); + if (overlayModel.isHighlightedIsolatedElementInPersistentOverlay(this.nodeInternal.id)) { + contextMenu.viewSection().appendItem(i18nString(UIStrings.exitIsolationMode), () => { + overlayModel.hideIsolatedElementInPersistentOverlay(this.nodeInternal.id); + }); + } else { + contextMenu.viewSection().appendItem(i18nString(UIStrings.enterIsolationMode), () => { + overlayModel.highlightIsolatedElementInPersistentOverlay(this.nodeInternal.id); + }); + } } populateScrollIntoView(contextMenu: UI.ContextMenu.ContextMenu): void { diff --git a/inspector_overlay/BUILD.gn b/inspector_overlay/BUILD.gn index d953b0285b..332a28a9de 100644 --- a/inspector_overlay/BUILD.gn +++ b/inspector_overlay/BUILD.gn @@ -14,10 +14,12 @@ action("build_inspector_overlay") { "$target_gen_dir/common.css", "$target_gen_dir/common.js", "$target_gen_dir/css_grid_label_helpers.js", + "$target_gen_dir/drag_resize_handler.js", "$target_gen_dir/highlight_common.js", "$target_gen_dir/highlight_container_query.js", "$target_gen_dir/highlight_flex_common.js", "$target_gen_dir/highlight_grid_common.js", + "$target_gen_dir/highlight_isolated_element.js", "$target_gen_dir/highlight_scroll_snap.js", "$target_gen_dir/tool_distances.js", "$target_gen_dir/tool_persistent.js", @@ -40,7 +42,7 @@ action("build_inspector_overlay") { args = [ "main.js", - "65000", # max_size in bytes + "70000", # max_size in bytes "--input_path", rebase_path(target_gen_dir, root_build_dir), "--output_path", @@ -77,10 +79,12 @@ ts_library("inspector_overlay") { sources = [ "common.ts", "css_grid_label_helpers.ts", + "drag_resize_handler.ts", "highlight_common.ts", "highlight_container_query.ts", "highlight_flex_common.ts", "highlight_grid_common.ts", + "highlight_isolated_element.ts", "highlight_scroll_snap.ts", "main.ts", "tool_distances.ts", diff --git a/inspector_overlay/debug/tool_persistent_isolated_element.html b/inspector_overlay/debug/tool_persistent_isolated_element.html new file mode 100644 index 0000000000..153119b3a8 --- /dev/null +++ b/inspector_overlay/debug/tool_persistent_isolated_element.html @@ -0,0 +1,75 @@ + + + + + + Debug + + + + + + + + + + + diff --git a/inspector_overlay/drag_resize_handler.ts b/inspector_overlay/drag_resize_handler.ts new file mode 100644 index 0000000000..53bf499218 --- /dev/null +++ b/inspector_overlay/drag_resize_handler.ts @@ -0,0 +1,142 @@ +// Copyright 2021 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export const enum ResizerType { + WIDTH = 'width', + HEIGHT = 'height', + BIDIRECTION = 'bidirection', +} + +export interface Draggable { + type: ResizerType; + initialWidth?: number; + initialHeight?: number; + update({width, height}: {width?: number, height?: number}): void; +} + +export interface Delegate { + getDraggable(x: number, y: number): Draggable|undefined; +} + +const cursorByResizerType = new Map([ + [ResizerType.WIDTH, 'ew-resize'], + [ResizerType.HEIGHT, 'ns-resize'], + [ResizerType.BIDIRECTION, 'nwse-resize'], +]); + +type OriginInfo = { + coord: number, + value: number, +}; + +export class DragResizeHandler { + private document: Document; + private delegate: Delegate; + private originX?: OriginInfo; + private originY?: OriginInfo; + private boundMousemove: (event: MouseEvent) => void; + private boundMousedown: (event: MouseEvent) => void; + + constructor(document: Document, delegate: Delegate) { + this.document = document; + this.delegate = delegate; + this.boundMousemove = this.onMousemove.bind(this); + this.boundMousedown = this.onMousedown.bind(this); + } + + install() { + this.document.body.addEventListener('mousemove', this.boundMousemove); + this.document.body.addEventListener('mousedown', this.boundMousedown); + } + + uninstall() { + this.document.body.removeEventListener('mousemove', this.boundMousemove); + this.document.body.removeEventListener('mousedown', this.boundMousedown); + } + + /** + * Updates the cursor style of the mouse is hovered over a resizeable area. + */ + private onMousemove(event: MouseEvent) { + const match = this.delegate.getDraggable(event.clientX, event.clientY); + if (!match) { + this.document.body.style.cursor = 'default'; + return; + } + this.document.body.style.cursor = cursorByResizerType.get(match.type) || 'default'; + } + + /** + * Starts dragging + */ + private onMousedown(event: MouseEvent) { + const match = this.delegate.getDraggable(event.clientX, event.clientY); + if (!match) { + return; + } + + const boundOnDrag = this.onDrag.bind(this, match); + + event.stopPropagation(); + event.preventDefault(); + + if (match.initialWidth !== undefined && + (match.type === ResizerType.WIDTH || match.type === ResizerType.BIDIRECTION)) { + this.originX = { + coord: Math.round(event.clientX), + value: match.initialWidth, + }; + } + + if (match.initialHeight !== undefined && + (match.type === ResizerType.HEIGHT || match.type === ResizerType.BIDIRECTION)) { + this.originY = { + coord: Math.round(event.clientY), + value: match.initialHeight, + }; + } + + this.document.body.removeEventListener('mousemove', this.boundMousemove); + this.document.body.style.cursor = cursorByResizerType.get(match.type) || 'default'; + + const endDrag = (event: Event) => { + event.stopPropagation(); + event.preventDefault(); + this.originX = undefined; + this.originY = undefined; + + this.document.body.style.cursor = 'default'; + this.document.body.removeEventListener('mousemove', boundOnDrag); + this.document.body.addEventListener('mousemove', this.boundMousemove); + }; + + this.document.body.addEventListener('mouseup', endDrag, {once: true}); + window.addEventListener('mouseout', endDrag, {once: true}); + + this.document.body.addEventListener('mousemove', boundOnDrag); + } + + /** + * Computes the new value while the cursor is being dragged and calls InspectorOverlayHost with the new value. + */ + private onDrag(match: Draggable, e: MouseEvent) { + if (!this.originX && !this.originY) { + return; + } + + let width: number|undefined; + let height: number|undefined; + if (this.originX) { + const delta = this.originX.coord - e.clientX; + width = Math.round(this.originX.value - delta); + } + + if (this.originY) { + const delta = this.originY.coord - e.clientY; + height = Math.round(this.originY.value - delta); + } + + match.update({width, height}); + } +} diff --git a/inspector_overlay/highlight_isolated_element.ts b/inspector_overlay/highlight_isolated_element.ts new file mode 100644 index 0000000000..50ed94d326 --- /dev/null +++ b/inspector_overlay/highlight_isolated_element.ts @@ -0,0 +1,62 @@ +// Copyright 2021 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import type {PathCommands} from './common.js'; +import {buildPath, emptyBounds, fillPathWithBoxStyle} from './highlight_common.js'; + +export interface IsolatedElementHighlight { + widthResizerBorder: PathCommands; + heightResizerBorder: PathCommands; + bidirectionResizerBorder: PathCommands; + currentX: number; + currentY: number; + currentWidth: number; + currentHeight: number; + highlightIndex: number; + isolationModeHighlightConfig: { + resizerColor: string, + resizerHandleColor: string, + maskColor: string, + }; +} + +export function drawIsolatedElementHighlight( + highlight: IsolatedElementHighlight, context: CanvasRenderingContext2D, canvasWidth: number, canvasHeight: number, + emulationScaleFactor: number) { + const {currentX, currentY, currentWidth, currentHeight, highlightIndex} = highlight; + // Draw a mask covering other area of the canvas. + context.save(); + context.fillStyle = highlight.isolationModeHighlightConfig.maskColor; + context.fillRect(0, 0, canvasWidth, canvasHeight); + context.clearRect(currentX, currentY, currentWidth, currentHeight); + context.restore(); + + // Draw the width resizer with handle bars. + const bounds = emptyBounds(); + const widthPath = buildPath(highlight.widthResizerBorder, bounds, emulationScaleFactor); + fillPathWithBoxStyle(context, widthPath, bounds, 0 /* angle */, { + fillColor: highlight.isolationModeHighlightConfig.resizerColor, + }); + + // Draw the height resizer with handle bars. + const heightPath = buildPath(highlight.heightResizerBorder, bounds, emulationScaleFactor); + fillPathWithBoxStyle(context, heightPath, bounds, 0 /* angle */, { + fillColor: highlight.isolationModeHighlightConfig.resizerColor, + }); + + // Draw the bidirection resizer with handle bars. + const bidirectionPath = buildPath(highlight.bidirectionResizerBorder, bounds, emulationScaleFactor); + fillPathWithBoxStyle(context, bidirectionPath, bounds, 0 /* angle */, { + fillColor: highlight.isolationModeHighlightConfig.resizerColor, + }); + + return { + widthPath, + heightPath, + bidirectionPath, + currentWidth, + currentHeight, + highlightIndex, + }; +} diff --git a/inspector_overlay/main.ts b/inspector_overlay/main.ts index 4211aaf6ad..b6e32eaa03 100644 --- a/inspector_overlay/main.ts +++ b/inspector_overlay/main.ts @@ -14,7 +14,9 @@ import highlightStyle from './tool_highlight.css'; // eslint-disable-line rulesd import {HighlightOverlay} from './tool_highlight.js'; // @ts-ignore Importing CSS is handled in Rollup. import pausedStyle from './tool_paused.css'; // eslint-disable-line rulesdir/es_modules_import +import type {PausedToolMessage} from './tool_paused.js'; import {PausedOverlay} from './tool_paused.js'; +import type {PersistentToolMessage} from './tool_persistent.js'; import {PersistentOverlay} from './tool_persistent.js'; // @ts-ignore Importing CSS is handled in Rollup. import screenshotStyle from './tool_screenshot.css'; // eslint-disable-line rulesdir/es_modules_import @@ -28,7 +30,7 @@ declare global { // eslint-disable-next-line @typescript-eslint/no-unused-vars interface Window { // eslint-disable-next-line @typescript-eslint/naming-convention - InspectorOverlayHost: {send(data: string): void}; + InspectorOverlayHost: {send(data: PausedToolMessage|PersistentToolMessage|string): void}; } } diff --git a/inspector_overlay/tool_highlight.ts b/inspector_overlay/tool_highlight.ts index 54639aa0d2..5f2d7cc6f6 100644 --- a/inspector_overlay/tool_highlight.ts +++ b/inspector_overlay/tool_highlight.ts @@ -41,6 +41,7 @@ import {drawLayoutGridHighlight} from './highlight_grid_common.js'; import type {ScrollSnapHighlight} from './highlight_scroll_snap.js'; import type {ContainerQueryHighlight} from './highlight_container_query.js'; import {drawContainerQueryHighlight} from './highlight_container_query.js'; +import type {IsolatedElementHighlight} from './highlight_isolated_element.js'; import {PersistentOverlay} from './tool_persistent.js'; interface Path { @@ -85,6 +86,7 @@ interface Highlight { flexInfo: FlexContainerHighlight[]; flexItemInfo: FlexItemHighlight[]; containerQueryInfo: ContainerQueryHighlight[]; + isolatedElementInfo: IsolatedElementHighlight[]; } export class HighlightOverlay extends Overlay { @@ -245,6 +247,10 @@ export class HighlightOverlay extends Overlay { this.persistentOverlay?.drawContainerQueryHighlight(highlight); } + drawIsolatedElementHighlight(highlight: IsolatedElementHighlight) { + this.persistentOverlay?.drawIsolatedElementHighlight(highlight); + } + private drawAxis(context: CanvasRenderingContext2D, rulerAtRight: boolean, rulerAtBottom: boolean) { context.save(); diff --git a/inspector_overlay/tool_paused.ts b/inspector_overlay/tool_paused.ts index 506d38cfce..d80c4afb15 100644 --- a/inspector_overlay/tool_paused.ts +++ b/inspector_overlay/tool_paused.ts @@ -4,6 +4,8 @@ import {Overlay} from './common.js'; +export type PausedToolMessage = 'resume'|'stepOver'; + export class PausedOverlay extends Overlay { private container!: HTMLElement; diff --git a/inspector_overlay/tool_persistent.ts b/inspector_overlay/tool_persistent.ts index 9a9575e718..954bd3134f 100644 --- a/inspector_overlay/tool_persistent.ts +++ b/inspector_overlay/tool_persistent.ts @@ -30,19 +30,74 @@ import type {ResetData} from './common.js'; import {Overlay} from './common.js'; +import type {Delegate} from './drag_resize_handler.js'; +import {DragResizeHandler, ResizerType} from './drag_resize_handler.js'; import type {ContainerQueryHighlight} from './highlight_container_query.js'; import {drawContainerQueryHighlight} from './highlight_container_query.js'; import type {FlexContainerHighlight} from './highlight_flex_common.js'; import {drawLayoutFlexContainerHighlight} from './highlight_flex_common.js'; import type {GridHighlight} from './highlight_grid_common.js'; import {drawLayoutGridHighlight} from './highlight_grid_common.js'; +import type {IsolatedElementHighlight} from './highlight_isolated_element.js'; +import {drawIsolatedElementHighlight} from './highlight_isolated_element.js'; import type {ScrollSnapHighlight} from './highlight_scroll_snap.js'; import {drawScrollSnapHighlight} from './highlight_scroll_snap.js'; +export type PersistentToolMessage = { + highlightType: string, + highlightIndex: number, + newWidth: string, + newHeight: string, + resizerType: ResizerType, +}; + +interface DraggableMetadata { + type: ResizerType; + highlightIndex: number; + initialWidth?: number; + initialHeight?: number; +} + +function makeDraggableDelegate(overlay: PersistentOverlay): Delegate { + return { + getDraggable: (x, y) => { + const result = overlay.isPointInDraggablePath(x, y); + if (!result) { + return; + } + + return { + type: result.type, + initialWidth: result.initialWidth, + initialHeight: result.initialHeight, + id: result.highlightIndex, + update: ({width, height}: {width?: number, height?: number}) => { + window.InspectorOverlayHost.send({ + highlightType: 'isolatedElement', + highlightIndex: result.highlightIndex, + newWidth: `${width}px`, + newHeight: `${height}px`, + resizerType: result.type, + }); + }, + }; + }, + }; +} + export class PersistentOverlay extends Overlay { private gridLabelState = {gridLayerCounter: 0}; private gridLabels!: HTMLElement; + private draggableBorders: Map = new Map(); + private dragHandler?: DragResizeHandler; reset(data: ResetData) { super.reset(data); @@ -70,12 +125,15 @@ export class PersistentOverlay extends Overlay { this.setCanvas(canvas); super.install(); + this.dragHandler?.install(); } uninstall() { this.document.body.classList.remove('fill'); this.document.body.innerHTML = ''; + this.draggableBorders = new Map(); super.uninstall(); + this.dragHandler?.uninstall(); } drawGridHighlight(highlight: GridHighlight) { @@ -105,4 +163,56 @@ export class PersistentOverlay extends Overlay { drawContainerQueryHighlight(highlight, this.context, this.emulationScaleFactor); this.context.restore(); } + + drawIsolatedElementHighlight(highlight: IsolatedElementHighlight) { + if (!this.dragHandler) { + this.dragHandler = new DragResizeHandler(this.document, makeDraggableDelegate(this)); + this.dragHandler.install(); + } + + this.context.save(); + const {widthPath, heightPath, bidirectionPath, currentWidth, currentHeight, highlightIndex} = + drawIsolatedElementHighlight( + highlight, this.context, this.canvasWidth, this.canvasHeight, this.emulationScaleFactor); + + this.draggableBorders.set(highlightIndex, { + widthPath, + heightPath, + bidirectionPath, + highlightIndex, + initialWidth: currentWidth, + initialHeight: currentHeight, + }); + this.context.restore(); + } + + isPointInDraggablePath(x: number, y: number): DraggableMetadata|undefined { + for (const {widthPath, heightPath, bidirectionPath, highlightIndex, initialWidth, initialHeight} of this + .draggableBorders.values()) { + if (this.context.isPointInPath(widthPath, x, y)) { + return { + type: ResizerType.WIDTH, + highlightIndex, + initialWidth, + }; + } + if (this.context.isPointInPath(heightPath, x, y)) { + return { + type: ResizerType.HEIGHT, + highlightIndex, + initialHeight, + }; + } + if (this.context.isPointInPath(bidirectionPath, x, y)) { + return { + type: ResizerType.BIDIRECTION, + highlightIndex, + initialWidth, + initialHeight, + }; + } + } + + return; + } } diff --git a/test/e2e/elements/BUILD.gn b/test/e2e/elements/BUILD.gn index b0e39b6141..af6bc2fff7 100644 --- a/test/e2e/elements/BUILD.gn +++ b/test/e2e/elements/BUILD.gn @@ -14,6 +14,7 @@ node_ts_library("elements") { "element-breadcrumbs_test.ts", "flexbox-editor_test.ts", "grid-editor_test.ts", + "isolation-mode_test.ts", "layout-pane_test.ts", "pseudo-states_test.ts", "reveal-correct-node_test.ts", diff --git a/test/e2e/elements/isolation-mode_test.ts b/test/e2e/elements/isolation-mode_test.ts new file mode 100644 index 0000000000..012aa4ed42 --- /dev/null +++ b/test/e2e/elements/isolation-mode_test.ts @@ -0,0 +1,15 @@ +// Copyright 2021 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import {goToResource} from '../../shared/helper.js'; +import {describe, it} from '../../shared/mocha-extensions.js'; +import {clickOnContextMenu} from '../helpers/sources-helpers.js'; + +describe('The Elements panel', async () => { + it('has a context menu item to enter Isolation Mode', async () => { + await goToResource('elements/css-container-queries.html'); + await clickOnContextMenu('[aria-label=""]', 'Enter Isolation Mode'); + await clickOnContextMenu('[aria-label=""]', 'Exit Isolation Mode'); + }); +}); diff --git a/test/unittests/front_end/core/sdk/OverlayPersistentHighlighter_test.ts b/test/unittests/front_end/core/sdk/OverlayPersistentHighlighter_test.ts index cbd7fa2e8b..b6ddc166bf 100644 --- a/test/unittests/front_end/core/sdk/OverlayPersistentHighlighter_test.ts +++ b/test/unittests/front_end/core/sdk/OverlayPersistentHighlighter_test.ts @@ -31,6 +31,7 @@ describeWithEnvironment('OverlayColorGenerator', () => { invoke_setShowFlexOverlays() {}, invoke_setShowScrollSnapOverlays() {}, invoke_setShowContainerQueryOverlays() {}, + invoke_setShowIsolatedElements() {}, }; }, }; @@ -80,4 +81,16 @@ describeWithEnvironment('OverlayColorGenerator', () => { highlighter.hideContainerQueryInOverlay(nodeId); assert(!highlighter.isContainerQueryHighlighted(nodeId)); }); + + it('is able to highlight isolated elements', () => { + const highlighter = new SDK.OverlayPersistentHighlighter.OverlayPersistentHighlighter(mockModel); + highlighter.highlightIsolatedElementInOverlay(nodeId); + assert(highlighter.isIsolatedElementHighlighted(nodeId)); + assert(!highlighter.isFlexHighlighted(nodeId)); + assert(!highlighter.isGridHighlighted(nodeId)); + assert(!highlighter.isScrollSnapHighlighted(nodeId)); + assert(!highlighter.isContainerQueryHighlighted(nodeId)); + highlighter.hideIsolatedElementInOverlay(nodeId); + assert(!highlighter.isIsolatedElementHighlighted(nodeId)); + }); });