// Copyright 2019 the V8 project 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 * as d3 from "d3"; import * as C from "../src/constants"; class Snapper { resizer: Resizer; sourceExpand: HTMLElement; sourceCollapse: HTMLElement; disassemblyExpand: HTMLElement; disassemblyCollapse: HTMLElement; rangesExpand: HTMLElement; rangesCollapse: HTMLElement; constructor(resizer: Resizer) { this.resizer = resizer; this.sourceExpand = document.getElementById(C.SOURCE_EXPAND_ID); this.sourceCollapse = document.getElementById(C.SOURCE_COLLAPSE_ID); this.disassemblyExpand = document.getElementById(C.DISASSEMBLY_EXPAND_ID); this.disassemblyCollapse = document.getElementById(C.DISASSEMBLY_COLLAPSE_ID); this.rangesExpand = document.getElementById(C.RANGES_EXPAND_ID); this.rangesCollapse = document.getElementById(C.RANGES_COLLAPSE_ID); document.getElementById("show-hide-source").addEventListener("click", () => { this.resizer.resizerLeft.classed("snapped", !this.resizer.resizerLeft.classed("snapped")); this.setSourceExpanded(!this.sourceExpand.classList.contains("invisible")); this.resizer.updatePanes(); }); document.getElementById("show-hide-disassembly").addEventListener("click", () => { this.resizer.resizerRight.classed("snapped", !this.resizer.resizerRight.classed("snapped")); this.setDisassemblyExpanded(!this.disassemblyExpand.classList.contains("invisible")); this.resizer.updatePanes(); }); document.getElementById("show-hide-ranges").addEventListener("click", () => { this.resizer.resizerRanges.classed("snapped", !this.resizer.resizerRanges.classed("snapped")); this.setRangesExpanded(!this.rangesExpand.classList.contains("invisible")); this.resizer.updatePanes(); }); } restoreExpandedState(): void { this.resizer.resizerLeft.classed("snapped", window.sessionStorage.getItem("expandedState-source") == "false"); this.resizer.resizerRight.classed("snapped", window.sessionStorage.getItem("expandedState-disassembly") == "false"); this.resizer.resizerRanges.classed("snapped", window.sessionStorage.getItem("expandedState-ranges") == "false"); this.setSourceExpanded(this.getLastExpandedState("source", true)); this.setDisassemblyExpanded(this.getLastExpandedState("disassembly", true)); this.setRangesExpanded(this.getLastExpandedState("ranges", true)); } getLastExpandedState(type: string, defaultState: boolean): boolean { const state = window.sessionStorage.getItem("expandedState-" + type); if (state === null) return defaultState; return state === 'true'; } sourceUpdate(isSourceExpanded: boolean): void { window.sessionStorage.setItem("expandedState-source", `${isSourceExpanded}`); this.sourceExpand.classList.toggle("invisible", isSourceExpanded); this.sourceCollapse.classList.toggle("invisible", !isSourceExpanded); document.getElementById("show-hide-ranges").style.marginLeft = isSourceExpanded ? null : "40px"; } setSourceExpanded(isSourceExpanded: boolean): void { this.sourceUpdate(isSourceExpanded); this.resizer.updateLeftWidth(); } disassemblyUpdate(isDisassemblyExpanded: boolean): void { window.sessionStorage.setItem("expandedState-disassembly", `${isDisassemblyExpanded}`); this.disassemblyExpand.classList.toggle("invisible", isDisassemblyExpanded); this.disassemblyCollapse.classList.toggle("invisible", !isDisassemblyExpanded); } setDisassemblyExpanded(isDisassemblyExpanded: boolean): void { this.disassemblyUpdate(isDisassemblyExpanded); this.resizer.updateRightWidth(); } rangesUpdate(isRangesExpanded: boolean): void { window.sessionStorage.setItem("expandedState-ranges", `${isRangesExpanded}`); this.rangesExpand.classList.toggle("invisible", isRangesExpanded); this.rangesCollapse.classList.toggle("invisible", !isRangesExpanded); } setRangesExpanded(isRangesExpanded: boolean): void { this.rangesUpdate(isRangesExpanded); this.resizer.updateRanges(); } } export class Resizer { snapper: Snapper; deadWidth: number; deadHeight: number; left: HTMLElement; right: HTMLElement; ranges: HTMLElement; middle: HTMLElement; sepLeft: number; sepRight: number; sepRangesHeight: number; panesUpdatedCallback: () => void; resizerRight: d3.Selection; resizerLeft: d3.Selection; resizerRanges: d3.Selection; private readonly SOURCE_PANE_DEFAULT_PERCENT = 1 / 4; private readonly DISASSEMBLY_PANE_DEFAULT_PERCENT = 3 / 4; private readonly RANGES_PANE_HEIGHT_DEFAULT_PERCENT = 3 / 4; private readonly RESIZER_RANGES_HEIGHT_BUFFER_PERCENTAGE = 5; private readonly RESIZER_SIZE = document.getElementById("resizer-ranges").offsetHeight; constructor(panesUpdatedCallback: () => void, deadWidth: number, deadHeight: number) { const resizer = this; resizer.panesUpdatedCallback = panesUpdatedCallback; resizer.deadWidth = deadWidth; resizer.deadHeight = deadHeight; resizer.left = document.getElementById(C.SOURCE_PANE_ID); resizer.right = document.getElementById(C.GENERATED_PANE_ID); resizer.ranges = document.getElementById(C.RANGES_PANE_ID); resizer.middle = document.getElementById("middle"); resizer.resizerLeft = d3.select('#resizer-left'); resizer.resizerRight = d3.select('#resizer-right'); resizer.resizerRanges = d3.select('#resizer-ranges'); // Set default sizes, if they weren't set. if (window.sessionStorage.getItem("source-pane-percent") === null) { window.sessionStorage.setItem("source-pane-percent", `${this.SOURCE_PANE_DEFAULT_PERCENT}`); } if (window.sessionStorage.getItem("disassembly-pane-percent") === null) { window.sessionStorage.setItem("disassembly-pane-percent", `${this.DISASSEMBLY_PANE_DEFAULT_PERCENT}`); } if (window.sessionStorage.getItem("ranges-pane-height-percent") === null) { window.sessionStorage.setItem("ranges-pane-height-percent", `${this.RANGES_PANE_HEIGHT_DEFAULT_PERCENT}`); } this.updateSizes(); const dragResizeLeft = d3.drag() .on('drag', function () { const x = d3.mouse(this.parentElement)[0]; resizer.sepLeft = Math.min(Math.max(0, x), resizer.sepRight); resizer.updatePanes(); }) .on('start', function () { resizer.resizerLeft.classed("dragged", true); }) .on('end', function () { // If the panel is close enough to the left, treat it as if it was pulled all the way to the lefg. const x = d3.mouse(this.parentElement)[0]; if (x <= deadWidth) { resizer.sepLeft = 0; resizer.updatePanes(); } // Snap if dragged all the way to the left. resizer.resizerLeft.classed("snapped", resizer.sepLeft === 0); if (!resizer.isLeftSnapped()) { window.sessionStorage.setItem("source-pane-percent", `${resizer.sepLeft / document.body.getBoundingClientRect().width}`); } resizer.snapper.setSourceExpanded(!resizer.isLeftSnapped()); resizer.resizerLeft.classed("dragged", false); }); resizer.resizerLeft.call(dragResizeLeft); const dragResizeRight = d3.drag() .on('drag', function () { const x = d3.mouse(this.parentElement)[0]; resizer.sepRight = Math.max(resizer.sepLeft, Math.min(x, document.body.getBoundingClientRect().width)); resizer.updatePanes(); }) .on('start', function () { resizer.resizerRight.classed("dragged", true); }) .on('end', function () { // If the panel is close enough to the right, treat it as if it was pulled all the way to the right. const x = d3.mouse(this.parentElement)[0]; const clientWidth = document.body.getBoundingClientRect().width; if (x >= (clientWidth - deadWidth)) { resizer.sepRight = clientWidth - 1; resizer.updatePanes(); } // Snap if dragged all the way to the right. resizer.resizerRight.classed("snapped", resizer.sepRight >= clientWidth - 1); if (!resizer.isRightSnapped()) { window.sessionStorage.setItem("disassembly-pane-percent", `${resizer.sepRight / clientWidth}`); } resizer.snapper.setDisassemblyExpanded(!resizer.isRightSnapped()); resizer.resizerRight.classed("dragged", false); }); resizer.resizerRight.call(dragResizeRight); const dragResizeRanges = d3.drag() .on('drag', function () { const y = d3.mouse(this.parentElement)[1]; resizer.sepRangesHeight = Math.max(100, Math.min(y, window.innerHeight) - resizer.RESIZER_RANGES_HEIGHT_BUFFER_PERCENTAGE); resizer.updatePanes(); }) .on('start', function () { resizer.resizerRanges.classed("dragged", true); }) .on('end', function () { // If the panel is close enough to the bottom, treat it as if it was pulled all the way to the bottom. const y = d3.mouse(this.parentElement)[1]; if (y >= (window.innerHeight - deadHeight)) { resizer.sepRangesHeight = window.innerHeight; resizer.updatePanes(); } // Snap if dragged all the way to the bottom. resizer.resizerRanges.classed("snapped", resizer.sepRangesHeight >= window.innerHeight - 1); if (!resizer.isRangesSnapped()) { window.sessionStorage.setItem("ranges-pane-height-percent", `${resizer.sepRangesHeight / window.innerHeight}`); } resizer.snapper.setRangesExpanded(!resizer.isRangesSnapped()); resizer.resizerRanges.classed("dragged", false); }); resizer.resizerRanges.call(dragResizeRanges); window.onresize = function () { resizer.updateSizes(); resizer.updatePanes(); }; resizer.snapper = new Snapper(resizer); resizer.snapper.restoreExpandedState(); } isLeftSnapped() { return this.resizerLeft.classed("snapped"); } isRightSnapped() { return this.resizerRight.classed("snapped"); } isRangesSnapped() { return this.resizerRanges.classed("snapped"); } updateRangesPane() { const clientHeight = window.innerHeight; const rangesIsHidden = this.ranges.style.visibility == "hidden"; let resizerSize = this.RESIZER_SIZE; if (rangesIsHidden) { resizerSize = 0; this.sepRangesHeight = clientHeight; } const rangeHeight = clientHeight - this.sepRangesHeight; this.ranges.style.height = rangeHeight + 'px'; const panelWidth = this.sepRight - this.sepLeft - (2 * resizerSize); this.ranges.style.width = panelWidth + 'px'; const multiview = document.getElementById("multiview"); if (multiview && multiview.style) { multiview.style.height = (this.sepRangesHeight - resizerSize) + 'px'; multiview.style.width = panelWidth + 'px'; } // Resize the range grid and labels. const rangeGrid = (this.ranges.getElementsByClassName("range-grid")[0] as HTMLElement); if (rangeGrid) { const yAxis = (this.ranges.getElementsByClassName("range-y-axis")[0] as HTMLElement); const rangeHeader = (this.ranges.getElementsByClassName("range-header")[0] as HTMLElement); const gridWidth = panelWidth - yAxis.clientWidth; rangeGrid.style.width = Math.floor(gridWidth - 1) + 'px'; // Take live ranges' right scrollbar into account. rangeHeader.style.width = (gridWidth - rangeGrid.offsetWidth + rangeGrid.clientWidth - 1) + 'px'; // Set resizer to horizontal. this.resizerRanges.style('width', panelWidth + 'px'); const rangeTitle = (this.ranges.getElementsByClassName("range-title-div")[0] as HTMLElement); const rangeHeaderLabel = (this.ranges.getElementsByClassName("range-header-label-x")[0] as HTMLElement); const gridHeight = rangeHeight - rangeHeader.clientHeight - rangeTitle.clientHeight - rangeHeaderLabel.clientHeight; rangeGrid.style.height = gridHeight + 'px'; // Take live ranges' bottom scrollbar into account. yAxis.style.height = (gridHeight - rangeGrid.offsetHeight + rangeGrid.clientHeight) + 'px'; } this.resizerRanges.style('ranges', this.ranges.style.height); } updatePanes() { this.left.style.width = this.sepLeft + 'px'; this.resizerLeft.style('left', this.sepLeft + 'px'); this.right.style.width = (document.body.getBoundingClientRect().width - this.sepRight) + 'px'; this.resizerRight.style('right', (document.body.getBoundingClientRect().width - this.sepRight - 1) + 'px'); this.updateRangesPane(); this.panesUpdatedCallback(); } updateRanges() { if (this.isRangesSnapped()) { this.sepRangesHeight = window.innerHeight; } else { const sepRangesHeight = window.sessionStorage.getItem("ranges-pane-height-percent"); this.sepRangesHeight = window.innerHeight * Number.parseFloat(sepRangesHeight); } } updateLeftWidth() { if (this.isLeftSnapped()) { this.sepLeft = 0; } else { const sepLeft = window.sessionStorage.getItem("source-pane-percent"); this.sepLeft = document.body.getBoundingClientRect().width * Number.parseFloat(sepLeft); } } updateRightWidth() { if (this.isRightSnapped()) { this.sepRight = document.body.getBoundingClientRect().width; } else { const sepRight = window.sessionStorage.getItem("disassembly-pane-percent"); this.sepRight = document.body.getBoundingClientRect().width * Number.parseFloat(sepRight); } } updateSizes() { this.updateLeftWidth(); this.updateRightWidth(); this.updateRanges(); } }