1// Copyright 2019 the V8 project authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5import * as d3 from "d3"; 6import * as C from "../src/constants"; 7 8class Snapper { 9 resizer: Resizer; 10 sourceExpand: HTMLElement; 11 sourceCollapse: HTMLElement; 12 disassemblyExpand: HTMLElement; 13 disassemblyCollapse: HTMLElement; 14 rangesExpand: HTMLElement; 15 rangesCollapse: HTMLElement; 16 17 constructor(resizer: Resizer) { 18 this.resizer = resizer; 19 this.sourceExpand = document.getElementById(C.SOURCE_EXPAND_ID); 20 this.sourceCollapse = document.getElementById(C.SOURCE_COLLAPSE_ID); 21 this.disassemblyExpand = document.getElementById(C.DISASSEMBLY_EXPAND_ID); 22 this.disassemblyCollapse = document.getElementById(C.DISASSEMBLY_COLLAPSE_ID); 23 this.rangesExpand = document.getElementById(C.RANGES_EXPAND_ID); 24 this.rangesCollapse = document.getElementById(C.RANGES_COLLAPSE_ID); 25 26 document.getElementById("show-hide-source").addEventListener("click", () => { 27 this.resizer.resizerLeft.classed("snapped", !this.resizer.resizerLeft.classed("snapped")); 28 this.setSourceExpanded(!this.sourceExpand.classList.contains("invisible")); 29 this.resizer.updatePanes(); 30 }); 31 document.getElementById("show-hide-disassembly").addEventListener("click", () => { 32 this.resizer.resizerRight.classed("snapped", !this.resizer.resizerRight.classed("snapped")); 33 this.setDisassemblyExpanded(!this.disassemblyExpand.classList.contains("invisible")); 34 this.resizer.updatePanes(); 35 }); 36 document.getElementById("show-hide-ranges").addEventListener("click", () => { 37 this.resizer.resizerRanges.classed("snapped", !this.resizer.resizerRanges.classed("snapped")); 38 this.setRangesExpanded(!this.rangesExpand.classList.contains("invisible")); 39 this.resizer.updatePanes(); 40 }); 41 } 42 43 restoreExpandedState(): void { 44 this.resizer.resizerLeft.classed("snapped", window.sessionStorage.getItem("expandedState-source") == "false"); 45 this.resizer.resizerRight.classed("snapped", window.sessionStorage.getItem("expandedState-disassembly") == "false"); 46 this.resizer.resizerRanges.classed("snapped", window.sessionStorage.getItem("expandedState-ranges") == "false"); 47 this.setSourceExpanded(this.getLastExpandedState("source", true)); 48 this.setDisassemblyExpanded(this.getLastExpandedState("disassembly", true)); 49 this.setRangesExpanded(this.getLastExpandedState("ranges", true)); 50 } 51 52 getLastExpandedState(type: string, defaultState: boolean): boolean { 53 const state = window.sessionStorage.getItem("expandedState-" + type); 54 if (state === null) return defaultState; 55 return state === 'true'; 56 } 57 58 sourceUpdate(isSourceExpanded: boolean): void { 59 window.sessionStorage.setItem("expandedState-source", `${isSourceExpanded}`); 60 this.sourceExpand.classList.toggle("invisible", isSourceExpanded); 61 this.sourceCollapse.classList.toggle("invisible", !isSourceExpanded); 62 document.getElementById("show-hide-ranges").style.marginLeft = isSourceExpanded ? null : "40px"; 63 } 64 65 setSourceExpanded(isSourceExpanded: boolean): void { 66 this.sourceUpdate(isSourceExpanded); 67 this.resizer.updateLeftWidth(); 68 } 69 70 disassemblyUpdate(isDisassemblyExpanded: boolean): void { 71 window.sessionStorage.setItem("expandedState-disassembly", `${isDisassemblyExpanded}`); 72 this.disassemblyExpand.classList.toggle("invisible", isDisassemblyExpanded); 73 this.disassemblyCollapse.classList.toggle("invisible", !isDisassemblyExpanded); 74 } 75 76 setDisassemblyExpanded(isDisassemblyExpanded: boolean): void { 77 this.disassemblyUpdate(isDisassemblyExpanded); 78 this.resizer.updateRightWidth(); 79 } 80 81 rangesUpdate(isRangesExpanded: boolean): void { 82 window.sessionStorage.setItem("expandedState-ranges", `${isRangesExpanded}`); 83 this.rangesExpand.classList.toggle("invisible", isRangesExpanded); 84 this.rangesCollapse.classList.toggle("invisible", !isRangesExpanded); 85 } 86 87 setRangesExpanded(isRangesExpanded: boolean): void { 88 this.rangesUpdate(isRangesExpanded); 89 this.resizer.updateRanges(); 90 } 91} 92 93export class Resizer { 94 snapper: Snapper; 95 deadWidth: number; 96 deadHeight: number; 97 left: HTMLElement; 98 right: HTMLElement; 99 ranges: HTMLElement; 100 middle: HTMLElement; 101 sepLeft: number; 102 sepRight: number; 103 sepRangesHeight: number; 104 panesUpdatedCallback: () => void; 105 resizerRight: d3.Selection<HTMLDivElement, any, any, any>; 106 resizerLeft: d3.Selection<HTMLDivElement, any, any, any>; 107 resizerRanges: d3.Selection<HTMLDivElement, any, any, any>; 108 109 private readonly SOURCE_PANE_DEFAULT_PERCENT = 1 / 4; 110 private readonly DISASSEMBLY_PANE_DEFAULT_PERCENT = 3 / 4; 111 private readonly RANGES_PANE_HEIGHT_DEFAULT_PERCENT = 3 / 4; 112 private readonly RESIZER_RANGES_HEIGHT_BUFFER_PERCENTAGE = 5; 113 private readonly RESIZER_SIZE = document.getElementById("resizer-ranges").offsetHeight; 114 115 constructor(panesUpdatedCallback: () => void, deadWidth: number, deadHeight: number) { 116 const resizer = this; 117 resizer.panesUpdatedCallback = panesUpdatedCallback; 118 resizer.deadWidth = deadWidth; 119 resizer.deadHeight = deadHeight; 120 resizer.left = document.getElementById(C.SOURCE_PANE_ID); 121 resizer.right = document.getElementById(C.GENERATED_PANE_ID); 122 resizer.ranges = document.getElementById(C.RANGES_PANE_ID); 123 resizer.middle = document.getElementById("middle"); 124 resizer.resizerLeft = d3.select('#resizer-left'); 125 resizer.resizerRight = d3.select('#resizer-right'); 126 resizer.resizerRanges = d3.select('#resizer-ranges'); 127 // Set default sizes, if they weren't set. 128 if (window.sessionStorage.getItem("source-pane-percent") === null) { 129 window.sessionStorage.setItem("source-pane-percent", `${this.SOURCE_PANE_DEFAULT_PERCENT}`); 130 } 131 if (window.sessionStorage.getItem("disassembly-pane-percent") === null) { 132 window.sessionStorage.setItem("disassembly-pane-percent", `${this.DISASSEMBLY_PANE_DEFAULT_PERCENT}`); 133 } 134 if (window.sessionStorage.getItem("ranges-pane-height-percent") === null) { 135 window.sessionStorage.setItem("ranges-pane-height-percent", `${this.RANGES_PANE_HEIGHT_DEFAULT_PERCENT}`); 136 } 137 138 this.updateSizes(); 139 140 const dragResizeLeft = d3.drag() 141 .on('drag', function () { 142 const x = d3.mouse(this.parentElement)[0]; 143 resizer.sepLeft = Math.min(Math.max(0, x), resizer.sepRight); 144 resizer.updatePanes(); 145 }) 146 .on('start', function () { 147 resizer.resizerLeft.classed("dragged", true); 148 }) 149 .on('end', function () { 150 // If the panel is close enough to the left, treat it as if it was pulled all the way to the lefg. 151 const x = d3.mouse(this.parentElement)[0]; 152 if (x <= deadWidth) { 153 resizer.sepLeft = 0; 154 resizer.updatePanes(); 155 } 156 // Snap if dragged all the way to the left. 157 resizer.resizerLeft.classed("snapped", resizer.sepLeft === 0); 158 if (!resizer.isLeftSnapped()) { 159 window.sessionStorage.setItem("source-pane-percent", `${resizer.sepLeft / document.body.getBoundingClientRect().width}`); 160 } 161 resizer.snapper.setSourceExpanded(!resizer.isLeftSnapped()); 162 resizer.resizerLeft.classed("dragged", false); 163 }); 164 resizer.resizerLeft.call(dragResizeLeft); 165 166 const dragResizeRight = d3.drag() 167 .on('drag', function () { 168 const x = d3.mouse(this.parentElement)[0]; 169 resizer.sepRight = Math.max(resizer.sepLeft, Math.min(x, document.body.getBoundingClientRect().width)); 170 resizer.updatePanes(); 171 }) 172 .on('start', function () { 173 resizer.resizerRight.classed("dragged", true); 174 }) 175 .on('end', function () { 176 // If the panel is close enough to the right, treat it as if it was pulled all the way to the right. 177 const x = d3.mouse(this.parentElement)[0]; 178 const clientWidth = document.body.getBoundingClientRect().width; 179 if (x >= (clientWidth - deadWidth)) { 180 resizer.sepRight = clientWidth - 1; 181 resizer.updatePanes(); 182 } 183 // Snap if dragged all the way to the right. 184 resizer.resizerRight.classed("snapped", resizer.sepRight >= clientWidth - 1); 185 if (!resizer.isRightSnapped()) { 186 window.sessionStorage.setItem("disassembly-pane-percent", `${resizer.sepRight / clientWidth}`); 187 } 188 resizer.snapper.setDisassemblyExpanded(!resizer.isRightSnapped()); 189 resizer.resizerRight.classed("dragged", false); 190 }); 191 resizer.resizerRight.call(dragResizeRight); 192 193 const dragResizeRanges = d3.drag() 194 .on('drag', function () { 195 const y = d3.mouse(this.parentElement)[1]; 196 resizer.sepRangesHeight = Math.max(100, Math.min(y, window.innerHeight) - resizer.RESIZER_RANGES_HEIGHT_BUFFER_PERCENTAGE); 197 resizer.updatePanes(); 198 }) 199 .on('start', function () { 200 resizer.resizerRanges.classed("dragged", true); 201 }) 202 .on('end', function () { 203 // If the panel is close enough to the bottom, treat it as if it was pulled all the way to the bottom. 204 const y = d3.mouse(this.parentElement)[1]; 205 if (y >= (window.innerHeight - deadHeight)) { 206 resizer.sepRangesHeight = window.innerHeight; 207 resizer.updatePanes(); 208 } 209 // Snap if dragged all the way to the bottom. 210 resizer.resizerRanges.classed("snapped", resizer.sepRangesHeight >= window.innerHeight - 1); 211 if (!resizer.isRangesSnapped()) { 212 window.sessionStorage.setItem("ranges-pane-height-percent", `${resizer.sepRangesHeight / window.innerHeight}`); 213 } 214 resizer.snapper.setRangesExpanded(!resizer.isRangesSnapped()); 215 resizer.resizerRanges.classed("dragged", false); 216 }); 217 resizer.resizerRanges.call(dragResizeRanges); 218 219 window.onresize = function () { 220 resizer.updateSizes(); 221 resizer.updatePanes(); 222 }; 223 resizer.snapper = new Snapper(resizer); 224 resizer.snapper.restoreExpandedState(); 225 } 226 227 isLeftSnapped() { 228 return this.resizerLeft.classed("snapped"); 229 } 230 231 isRightSnapped() { 232 return this.resizerRight.classed("snapped"); 233 } 234 235 isRangesSnapped() { 236 return this.resizerRanges.classed("snapped"); 237 } 238 239 updateRangesPane() { 240 const clientHeight = window.innerHeight; 241 const rangesIsHidden = this.ranges.style.visibility == "hidden"; 242 let resizerSize = this.RESIZER_SIZE; 243 if (rangesIsHidden) { 244 resizerSize = 0; 245 this.sepRangesHeight = clientHeight; 246 } 247 248 const rangeHeight = clientHeight - this.sepRangesHeight; 249 this.ranges.style.height = rangeHeight + 'px'; 250 const panelWidth = this.sepRight - this.sepLeft - (2 * resizerSize); 251 this.ranges.style.width = panelWidth + 'px'; 252 const multiview = document.getElementById("multiview"); 253 if (multiview && multiview.style) { 254 multiview.style.height = (this.sepRangesHeight - resizerSize) + 'px'; 255 multiview.style.width = panelWidth + 'px'; 256 } 257 258 // Resize the range grid and labels. 259 const rangeGrid = (this.ranges.getElementsByClassName("range-grid")[0] as HTMLElement); 260 if (rangeGrid) { 261 const yAxis = (this.ranges.getElementsByClassName("range-y-axis")[0] as HTMLElement); 262 const rangeHeader = (this.ranges.getElementsByClassName("range-header")[0] as HTMLElement); 263 264 const gridWidth = panelWidth - yAxis.clientWidth; 265 rangeGrid.style.width = Math.floor(gridWidth - 1) + 'px'; 266 // Take live ranges' right scrollbar into account. 267 rangeHeader.style.width = (gridWidth - rangeGrid.offsetWidth + rangeGrid.clientWidth - 1) + 'px'; 268 // Set resizer to horizontal. 269 this.resizerRanges.style('width', panelWidth + 'px'); 270 271 const rangeTitle = (this.ranges.getElementsByClassName("range-title-div")[0] as HTMLElement); 272 const rangeHeaderLabel = (this.ranges.getElementsByClassName("range-header-label-x")[0] as HTMLElement); 273 const gridHeight = rangeHeight - rangeHeader.clientHeight - rangeTitle.clientHeight - rangeHeaderLabel.clientHeight; 274 rangeGrid.style.height = gridHeight + 'px'; 275 // Take live ranges' bottom scrollbar into account. 276 yAxis.style.height = (gridHeight - rangeGrid.offsetHeight + rangeGrid.clientHeight) + 'px'; 277 } 278 this.resizerRanges.style('ranges', this.ranges.style.height); 279 } 280 281 updatePanes() { 282 this.left.style.width = this.sepLeft + 'px'; 283 this.resizerLeft.style('left', this.sepLeft + 'px'); 284 this.right.style.width = (document.body.getBoundingClientRect().width - this.sepRight) + 'px'; 285 this.resizerRight.style('right', (document.body.getBoundingClientRect().width - this.sepRight - 1) + 'px'); 286 this.updateRangesPane(); 287 this.panesUpdatedCallback(); 288 } 289 290 updateRanges() { 291 if (this.isRangesSnapped()) { 292 this.sepRangesHeight = window.innerHeight; 293 } else { 294 const sepRangesHeight = window.sessionStorage.getItem("ranges-pane-height-percent"); 295 this.sepRangesHeight = window.innerHeight * Number.parseFloat(sepRangesHeight); 296 } 297 } 298 299 updateLeftWidth() { 300 if (this.isLeftSnapped()) { 301 this.sepLeft = 0; 302 } else { 303 const sepLeft = window.sessionStorage.getItem("source-pane-percent"); 304 this.sepLeft = document.body.getBoundingClientRect().width * Number.parseFloat(sepLeft); 305 } 306 } 307 308 updateRightWidth() { 309 if (this.isRightSnapped()) { 310 this.sepRight = document.body.getBoundingClientRect().width; 311 } else { 312 const sepRight = window.sessionStorage.getItem("disassembly-pane-percent"); 313 this.sepRight = document.body.getBoundingClientRect().width * Number.parseFloat(sepRight); 314 } 315 } 316 317 updateSizes() { 318 this.updateLeftWidth(); 319 this.updateRightWidth(); 320 this.updateRanges(); 321 } 322} 323