Home | History | Annotate | Download | only in driver
      1 // Copyright 2017 Google Inc. All Rights Reserved.
      2 //
      3 // Licensed under the Apache License, Version 2.0 (the "License");
      4 // you may not use this file except in compliance with the License.
      5 // You may obtain a copy of the License at
      6 //
      7 //     http://www.apache.org/licenses/LICENSE-2.0
      8 //
      9 // Unless required by applicable law or agreed to in writing, software
     10 // distributed under the License is distributed on an "AS IS" BASIS,
     11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     12 // See the License for the specific language governing permissions and
     13 // limitations under the License.
     14 
     15 package driver
     16 
     17 import "html/template"
     18 
     19 // addTemplates adds a set of template definitions to templates.
     20 func addTemplates(templates *template.Template) {
     21 	template.Must(templates.Parse(`
     22 {{define "css"}}
     23 <style type="text/css">
     24 html {
     25   height: 100%;
     26   min-height: 100%;
     27   margin: 0px;
     28 }
     29 body {
     30   margin: 0px;
     31   width: 100%;
     32   height: 100%;
     33   min-height: 100%;
     34   overflow: hidden;
     35 }
     36 #graphcontainer {
     37   display: flex;
     38   flex-direction: column;
     39   height: 100%;
     40   min-height: 100%;
     41   width: 100%;
     42   min-width: 100%;
     43   margin: 0px;
     44 }
     45 #graph {
     46   flex: 1 1 auto;
     47   overflow: hidden;
     48 }
     49 svg {
     50   width: 100%;
     51   height: auto;
     52 }
     53 button {
     54   margin-top: 5px;
     55   margin-bottom: 5px;
     56 }
     57 #detailtext {
     58   display: none;
     59   position: fixed;
     60   top: 20px;
     61   right: 10px;
     62   background-color: #ffffff;
     63   min-width: 160px;
     64   border: 1px solid #888;
     65   box-shadow: 4px 4px 4px 0px rgba(0,0,0,0.2);
     66   z-index: 1;
     67 }
     68 #closedetails {
     69   float: right;
     70   margin: 2px;
     71 }
     72 #home {
     73   font-size: 14pt;
     74   padding-left: 0.5em;
     75   padding-right: 0.5em;
     76   float: right;
     77 }
     78 .menubar {
     79   display: inline-block;
     80   background-color: #f8f8f8;
     81   border: 1px solid #ccc;
     82   width: 100%;
     83 }
     84 .menu-header {
     85   position: relative;
     86   display: inline-block;
     87   padding: 2px 2px;
     88   font-size: 14pt;
     89 }
     90 .menu {
     91   display: none;
     92   position: absolute;
     93   background-color: #f8f8f8;
     94   border: 1px solid #888;
     95   box-shadow: 4px 4px 4px 0px rgba(0,0,0,0.2);
     96   z-index: 1;
     97   margin-top: 2px;
     98   left: 0px;
     99   min-width: 5em;
    100 }
    101 .menu-header, .menu {
    102   cursor: default;
    103   user-select: none;
    104   -moz-user-select: none;
    105   -ms-user-select: none;
    106   -webkit-user-select: none;
    107 }
    108 .menu hr {
    109   background-color: #fff;
    110   margin-top: 0px;
    111   margin-bottom: 0px;
    112 }
    113 .menu a, .menu button {
    114   display: block;
    115   width: 100%;
    116   margin: 0px;
    117   padding: 2px 0px 2px 0px;
    118   text-align: left;
    119   text-decoration: none;
    120   color: #000;
    121   background-color: #f8f8f8;
    122   font-size: 12pt;
    123   border: none;
    124 }
    125 .menu-header:hover {
    126   background-color: #ccc;
    127 }
    128 .menu a:hover, .menu button:hover {
    129   background-color: #ccc;
    130 }
    131 .menu a.disabled {
    132   color: gray;
    133   pointer-events: none;
    134 }
    135 #searchbox {
    136   margin-left: 10pt;
    137 }
    138 #bodycontainer {
    139   width: 100%;
    140   height: 100%;
    141   max-height: 100%;
    142   overflow: scroll;
    143   padding-top: 5px;
    144 }
    145 #toptable {
    146   border-spacing: 0px;
    147   width: 100%;
    148   padding-bottom: 1em;
    149 }
    150 #toptable tr th {
    151   border-bottom: 1px solid black;
    152   text-align: right;
    153   padding-left: 1em;
    154   padding-top: 0.2em;
    155   padding-bottom: 0.2em;
    156 }
    157 #toptable tr td {
    158   padding-left: 1em;
    159   font: monospace;
    160   text-align: right;
    161   white-space: nowrap;
    162   cursor: default;
    163 }
    164 #toptable tr th:nth-child(6),
    165 #toptable tr th:nth-child(7),
    166 #toptable tr td:nth-child(6),
    167 #toptable tr td:nth-child(7) {
    168   text-align: left;
    169 }
    170 #toptable tr td:nth-child(6) {
    171   max-width: 30em;  // Truncate very long names
    172   overflow: hidden;
    173 }
    174 #flathdr1, #flathdr2, #cumhdr1, #cumhdr2, #namehdr {
    175   cursor: ns-resize;
    176 }
    177 .hilite {
    178   background-color: #ccf;
    179 }
    180 </style>
    181 {{end}}
    182 
    183 {{define "header"}}
    184 <div id="detailtext">
    185 <button id="closedetails">Close</button>
    186 {{range .Legend}}<div>{{.}}</div>{{end}}
    187 </div>
    188 
    189 <div class="menubar">
    190 
    191 <div class="menu-header">
    192 View
    193 <div class="menu">
    194 <a title="{{.Help.top}}"  href="/top" id="topbtn">Top</a>
    195 <a title="{{.Help.graph}}" href="/" id="graphbtn">Graph</a>
    196 <a title="{{.Help.peek}}" href="/peek" id="peek">Peek</a>
    197 <a title="{{.Help.list}}" href="/source" id="list">Source</a>
    198 <a title="{{.Help.disasm}}" href="/disasm" id="disasm">Disassemble</a>
    199 <hr>
    200 <button title="{{.Help.details}}" id="details">Details</button>
    201 </div>
    202 </div>
    203 
    204 <div class="menu-header">
    205 Refine
    206 <div class="menu">
    207 <a title="{{.Help.focus}}" href="{{.BaseURL}}" id="focus">Focus</a>
    208 <a title="{{.Help.ignore}}" href="{{.BaseURL}}" id="ignore">Ignore</a>
    209 <a title="{{.Help.hide}}" href="{{.BaseURL}}" id="hide">Hide</a>
    210 <a title="{{.Help.show}}" href="{{.BaseURL}}" id="show">Show</a>
    211 <hr>
    212 <a title="{{.Help.reset}}" href="{{.BaseURL}}">Reset</a>
    213 </div>
    214 </div>
    215 
    216 <input id="searchbox" type="text" placeholder="Search regexp" autocomplete="off" autocapitalize="none" size=40>
    217 
    218 <span id="home">{{.Title}}</span>
    219 
    220 </div> <!-- menubar -->
    221 
    222 <div id="errors">{{range .Errors}}<div>{{.}}</div>{{end}}</div>
    223 {{end}}
    224 
    225 {{define "graph" -}}
    226 <!DOCTYPE html>
    227 <html>
    228 <head>
    229 <meta charset="utf-8">
    230 <title>{{.Title}}</title>
    231 {{template "css" .}}
    232 </head>
    233 <body>
    234 
    235 {{template "header" .}}
    236 <div id="graphcontainer">
    237 <div id="graph">
    238 {{.HTMLBody}}
    239 </div>
    240 
    241 </div>
    242 {{template "script" .}}
    243 <script>viewer({{.BaseURL}}, {{.Nodes}})</script>
    244 </body>
    245 </html>
    246 {{end}}
    247 
    248 {{define "script"}}
    249 <script>
    250 // Make svg pannable and zoomable.
    251 // Call clickHandler(t) if a click event is caught by the pan event handlers.
    252 function initPanAndZoom(svg, clickHandler) {
    253   'use strict';
    254 
    255   // Current mouse/touch handling mode
    256   const IDLE = 0
    257   const MOUSEPAN = 1
    258   const TOUCHPAN = 2
    259   const TOUCHZOOM = 3
    260   let mode = IDLE
    261 
    262   // State needed to implement zooming.
    263   let currentScale = 1.0
    264   const initWidth = svg.viewBox.baseVal.width
    265   const initHeight = svg.viewBox.baseVal.height
    266 
    267   // State needed to implement panning.
    268   let panLastX = 0      // Last event X coordinate
    269   let panLastY = 0      // Last event Y coordinate
    270   let moved = false     // Have we seen significant movement
    271   let touchid = null    // Current touch identifier
    272 
    273   // State needed for pinch zooming
    274   let touchid2 = null     // Second id for pinch zooming
    275   let initGap = 1.0       // Starting gap between two touches
    276   let initScale = 1.0     // currentScale when pinch zoom started
    277   let centerPoint = null  // Center point for scaling
    278 
    279   // Convert event coordinates to svg coordinates.
    280   function toSvg(x, y) {
    281     const p = svg.createSVGPoint()
    282     p.x = x
    283     p.y = y
    284     let m = svg.getCTM()
    285     if (m == null) m = svg.getScreenCTM()  // Firefox workaround.
    286     return p.matrixTransform(m.inverse())
    287   }
    288 
    289   // Change the scaling for the svg to s, keeping the point denoted
    290   // by u (in svg coordinates]) fixed at the same screen location.
    291   function rescale(s, u) {
    292     // Limit to a good range.
    293     if (s < 0.2) s = 0.2
    294     if (s > 10.0) s = 10.0
    295 
    296     currentScale = s
    297 
    298     // svg.viewBox defines the visible portion of the user coordinate
    299     // system.  So to magnify by s, divide the visible portion by s,
    300     // which will then be stretched to fit the viewport.
    301     const vb = svg.viewBox
    302     const w1 = vb.baseVal.width
    303     const w2 = initWidth / s
    304     const h1 = vb.baseVal.height
    305     const h2 = initHeight / s
    306     vb.baseVal.width = w2
    307     vb.baseVal.height = h2
    308 
    309     // We also want to adjust vb.baseVal.x so that u.x remains at same
    310     // screen X coordinate.  In other words, want to change it from x1 to x2
    311     // so that:
    312     //     (u.x - x1) / w1 = (u.x - x2) / w2
    313     // Simplifying that, we get
    314     //     (u.x - x1) * (w2 / w1) = u.x - x2
    315     //     x2 = u.x - (u.x - x1) * (w2 / w1)
    316     vb.baseVal.x = u.x - (u.x - vb.baseVal.x) * (w2 / w1)
    317     vb.baseVal.y = u.y - (u.y - vb.baseVal.y) * (h2 / h1)
    318   }
    319 
    320   function handleWheel(e) {
    321     if (e.deltaY == 0) return
    322     // Change scale factor by 1.1 or 1/1.1
    323     rescale(currentScale * (e.deltaY < 0 ? 1.1 : (1/1.1)),
    324             toSvg(e.offsetX, e.offsetY))
    325   }
    326 
    327   function setMode(m) {
    328     mode = m
    329     touchid = null
    330     touchid2 = null
    331   }
    332 
    333   function panStart(x, y) {
    334     moved = false
    335     panLastX = x
    336     panLastY = y
    337   }
    338 
    339   function panMove(x, y) {
    340     let dx = x - panLastX
    341     let dy = y - panLastY
    342     if (Math.abs(dx) <= 2 && Math.abs(dy) <= 2) return  // Ignore tiny moves
    343 
    344     moved = true
    345     panLastX = x
    346     panLastY = y
    347 
    348     // Firefox workaround: get dimensions from parentNode.
    349     const swidth = svg.clientWidth || svg.parentNode.clientWidth
    350     const sheight = svg.clientHeight || svg.parentNode.clientHeight
    351 
    352     // Convert deltas from screen space to svg space.
    353     dx *= (svg.viewBox.baseVal.width / swidth)
    354     dy *= (svg.viewBox.baseVal.height / sheight)
    355 
    356     svg.viewBox.baseVal.x -= dx
    357     svg.viewBox.baseVal.y -= dy
    358   }
    359 
    360   function handleScanStart(e) {
    361     if (e.button != 0) return  // Do not catch right-clicks etc.
    362     setMode(MOUSEPAN)
    363     panStart(e.clientX, e.clientY)
    364     e.preventDefault()
    365     svg.addEventListener("mousemove", handleScanMove)
    366   }
    367 
    368   function handleScanMove(e) {
    369     if (e.buttons == 0) {
    370       // Missed an end event, perhaps because mouse moved outside window.
    371       setMode(IDLE)
    372       svg.removeEventListener("mousemove", handleScanMove)
    373       return
    374     }
    375     if (mode == MOUSEPAN) panMove(e.clientX, e.clientY)
    376   }
    377 
    378   function handleScanEnd(e) {
    379     if (mode == MOUSEPAN) panMove(e.clientX, e.clientY)
    380     setMode(IDLE)
    381     svg.removeEventListener("mousemove", handleScanMove)
    382     if (!moved) clickHandler(e.target)
    383   }
    384 
    385   // Find touch object with specified identifier.
    386   function findTouch(tlist, id) {
    387     for (const t of tlist) {
    388       if (t.identifier == id) return t
    389     }
    390     return null
    391   }
    392 
    393  // Return distance between two touch points
    394   function touchGap(t1, t2) {
    395     const dx = t1.clientX - t2.clientX
    396     const dy = t1.clientY - t2.clientY
    397     return Math.hypot(dx, dy)
    398   }
    399 
    400   function handleTouchStart(e) {
    401     if (mode == IDLE && e.changedTouches.length == 1) {
    402       // Start touch based panning
    403       const t = e.changedTouches[0]
    404       setMode(TOUCHPAN)
    405       touchid = t.identifier
    406       panStart(t.clientX, t.clientY)
    407       e.preventDefault()
    408     } else if (mode == TOUCHPAN && e.touches.length == 2) {
    409       // Start pinch zooming
    410       setMode(TOUCHZOOM)
    411       const t1 = e.touches[0]
    412       const t2 = e.touches[1]
    413       touchid = t1.identifier
    414       touchid2 = t2.identifier
    415       initScale = currentScale
    416       initGap = touchGap(t1, t2)
    417       centerPoint = toSvg((t1.clientX + t2.clientX) / 2,
    418                           (t1.clientY + t2.clientY) / 2)
    419       e.preventDefault()
    420     }
    421   }
    422 
    423   function handleTouchMove(e) {
    424     if (mode == TOUCHPAN) {
    425       const t = findTouch(e.changedTouches, touchid)
    426       if (t == null) return
    427       if (e.touches.length != 1) {
    428         setMode(IDLE)
    429         return
    430       }
    431       panMove(t.clientX, t.clientY)
    432       e.preventDefault()
    433     } else if (mode == TOUCHZOOM) {
    434       // Get two touches; new gap; rescale to ratio.
    435       const t1 = findTouch(e.touches, touchid)
    436       const t2 = findTouch(e.touches, touchid2)
    437       if (t1 == null || t2 == null) return
    438       const gap = touchGap(t1, t2)
    439       rescale(initScale * gap / initGap, centerPoint)
    440       e.preventDefault()
    441     }
    442   }
    443 
    444   function handleTouchEnd(e) {
    445     if (mode == TOUCHPAN) {
    446       const t = findTouch(e.changedTouches, touchid)
    447       if (t == null) return
    448       panMove(t.clientX, t.clientY)
    449       setMode(IDLE)
    450       e.preventDefault()
    451       if (!moved) clickHandler(t.target)
    452     } else if (mode == TOUCHZOOM) {
    453       setMode(IDLE)
    454       e.preventDefault()
    455     }
    456   }
    457 
    458   svg.addEventListener("mousedown", handleScanStart)
    459   svg.addEventListener("mouseup", handleScanEnd)
    460   svg.addEventListener("touchstart", handleTouchStart)
    461   svg.addEventListener("touchmove", handleTouchMove)
    462   svg.addEventListener("touchend", handleTouchEnd)
    463   svg.addEventListener("wheel", handleWheel, true)
    464 }
    465 
    466 function initMenus() {
    467   'use strict';
    468 
    469   let activeMenu = null;
    470   let activeMenuHdr = null;
    471 
    472   function cancelActiveMenu() {
    473     if (activeMenu == null) return;
    474     activeMenu.style.display = "none";
    475     activeMenu = null;
    476     activeMenuHdr = null;
    477   }
    478 
    479   // Set click handlers on every menu header.
    480   for (const menu of document.getElementsByClassName("menu")) {
    481     const hdr = menu.parentElement;
    482     if (hdr == null) return;
    483     function showMenu(e) {
    484       // menu is a child of hdr, so this event can fire for clicks
    485       // inside menu. Ignore such clicks.
    486       if (e.target != hdr) return;
    487       activeMenu = menu;
    488       activeMenuHdr = hdr;
    489       menu.style.display = "block";
    490     }
    491     hdr.addEventListener("mousedown", showMenu);
    492     hdr.addEventListener("touchstart", showMenu);
    493   }
    494 
    495   // If there is an active menu and a down event outside, retract the menu.
    496   for (const t of ["mousedown", "touchstart"]) {
    497     document.addEventListener(t, (e) => {
    498       // Note: to avoid unnecessary flicker, if the down event is inside
    499       // the active menu header, do not retract the menu.
    500       if (activeMenuHdr != e.target.closest(".menu-header")) {
    501         cancelActiveMenu();
    502       }
    503     }, { passive: true, capture: true });
    504   }
    505 
    506   // If there is an active menu and an up event inside, retract the menu.
    507   document.addEventListener("mouseup", (e) => {
    508     if (activeMenu == e.target.closest(".menu")) {
    509       cancelActiveMenu();
    510     }
    511   }, { passive: true, capture: true });
    512 }
    513 
    514 function viewer(baseUrl, nodes) {
    515   'use strict';
    516 
    517   // Elements
    518   const search = document.getElementById("searchbox")
    519   const graph0 = document.getElementById("graph0")
    520   const svg = (graph0 == null ? null : graph0.parentElement)
    521   const toptable = document.getElementById("toptable")
    522 
    523   let regexpActive = false
    524   let selected = new Map()
    525   let origFill = new Map()
    526   let searchAlarm = null
    527   let buttonsEnabled = true
    528 
    529   function handleDetails() {
    530     const detailsText = document.getElementById("detailtext")
    531     if (detailsText != null) detailsText.style.display = "block"
    532   }
    533 
    534   function handleCloseDetails() {
    535     const detailsText = document.getElementById("detailtext")
    536     if (detailsText != null) detailsText.style.display = "none"
    537   }
    538 
    539   function handleKey(e) {
    540     if (e.keyCode != 13) return
    541     window.location.href =
    542         updateUrl(new URL({{.BaseURL}}, window.location.href), "f")
    543     e.preventDefault()
    544   }
    545 
    546   function handleSearch() {
    547     // Delay expensive processing so a flurry of key strokes is handled once.
    548     if (searchAlarm != null) {
    549       clearTimeout(searchAlarm)
    550     }
    551     searchAlarm = setTimeout(selectMatching, 300)
    552 
    553     regexpActive = true
    554     updateButtons()
    555   }
    556 
    557   function selectMatching() {
    558     searchAlarm = null
    559     let re = null
    560     if (search.value != "") {
    561       try {
    562         re = new RegExp(search.value)
    563       } catch (e) {
    564         // TODO: Display error state in search box
    565         return
    566       }
    567     }
    568 
    569     function match(text) {
    570       return re != null && re.test(text)
    571     }
    572 
    573     // drop currently selected items that do not match re.
    574     selected.forEach(function(v, n) {
    575       if (!match(nodes[n])) {
    576         unselect(n, document.getElementById("node" + n))
    577       }
    578     })
    579 
    580     // add matching items that are not currently selected.
    581     for (let n = 0; n < nodes.length; n++) {
    582       if (!selected.has(n) && match(nodes[n])) {
    583         select(n, document.getElementById("node" + n))
    584       }
    585     }
    586 
    587     updateButtons()
    588   }
    589 
    590   function toggleSvgSelect(elem) {
    591     // Walk up to immediate child of graph0
    592     while (elem != null && elem.parentElement != graph0) {
    593       elem = elem.parentElement
    594     }
    595     if (!elem) return
    596 
    597     // Disable regexp mode.
    598     regexpActive = false
    599 
    600     const n = nodeId(elem)
    601     if (n < 0) return
    602     if (selected.has(n)) {
    603       unselect(n, elem)
    604     } else {
    605       select(n, elem)
    606     }
    607     updateButtons()
    608   }
    609 
    610   function unselect(n, elem) {
    611     if (elem == null) return
    612     selected.delete(n)
    613     setBackground(elem, false)
    614   }
    615 
    616   function select(n, elem) {
    617     if (elem == null) return
    618     selected.set(n, true)
    619     setBackground(elem, true)
    620   }
    621 
    622   function nodeId(elem) {
    623     const id = elem.id
    624     if (!id) return -1
    625     if (!id.startsWith("node")) return -1
    626     const n = parseInt(id.slice(4), 10)
    627     if (isNaN(n)) return -1
    628     if (n < 0 || n >= nodes.length) return -1
    629     return n
    630   }
    631 
    632   function setBackground(elem, set) {
    633     // Handle table row highlighting.
    634     if (elem.nodeName == "TR") {
    635       elem.classList.toggle("hilite", set)
    636       return
    637     }
    638 
    639     // Handle svg element highlighting.
    640     const p = findPolygon(elem)
    641     if (p != null) {
    642       if (set) {
    643         origFill.set(p, p.style.fill)
    644         p.style.fill = "#ccccff"
    645       } else if (origFill.has(p)) {
    646         p.style.fill = origFill.get(p)
    647       }
    648     }
    649   }
    650 
    651   function findPolygon(elem) {
    652     if (elem.localName == "polygon") return elem
    653     for (const c of elem.children) {
    654       const p = findPolygon(c)
    655       if (p != null) return p
    656     }
    657     return null
    658   }
    659 
    660   // convert a string to a regexp that matches that string.
    661   function quotemeta(str) {
    662     return str.replace(/([\\\.?+*\[\](){}|^$])/g, '\\$1')
    663   }
    664 
    665   // Update id's href to reflect current selection whenever it is
    666   // liable to be followed.
    667   function makeLinkDynamic(id) {
    668     const elem = document.getElementById(id)
    669     if (elem == null) return
    670 
    671     // Most links copy current selection into the "f" parameter,
    672     // but Refine menu links are different.
    673     let param = "f"
    674     if (id == "ignore") param = "i"
    675     if (id == "hide") param = "h"
    676     if (id == "show") param = "s"
    677 
    678     // We update on mouseenter so middle-click/right-click work properly.
    679     elem.addEventListener("mouseenter", updater)
    680     elem.addEventListener("touchstart", updater)
    681 
    682     function updater() {
    683       elem.href = updateUrl(new URL(elem.href), param)
    684     }
    685   }
    686 
    687   // Update URL to reflect current selection.
    688   function updateUrl(url, param) {
    689     url.hash = ""
    690 
    691     // The selection can be in one of two modes: regexp-based or
    692     // list-based.  Construct regular expression depending on mode.
    693     let re = regexpActive
    694         ? search.value
    695         : Array.from(selected.keys()).map(key => quotemeta(nodes[key])).join("|")
    696 
    697     // Copy params from this page's URL.
    698     const params = url.searchParams
    699     for (const p of new URLSearchParams(window.location.search)) {
    700       params.set(p[0], p[1])
    701     }
    702 
    703     if (re != "") {
    704       // For focus/show, forget old parameter.  For others, add to re.
    705       if (param != "f" && param != "s" && params.has(param)) {
    706         const old = params.get(param)
    707          if (old != "") {
    708           re += "|" + old
    709         }
    710       }
    711       params.set(param, re)
    712     } else {
    713       params.delete(param)
    714     }
    715 
    716     return url.toString()
    717   }
    718 
    719   function handleTopClick(e) {
    720     // Walk back until we find TR and then get the Name column (index 5)
    721     let elem = e.target
    722     while (elem != null && elem.nodeName != "TR") {
    723       elem = elem.parentElement
    724     }
    725     if (elem == null || elem.children.length < 6) return
    726 
    727     e.preventDefault()
    728     const tr = elem
    729     const td = elem.children[5]
    730     if (td.nodeName != "TD") return
    731     const name = td.innerText
    732     const index = nodes.indexOf(name)
    733     if (index < 0) return
    734 
    735     // Disable regexp mode.
    736     regexpActive = false
    737 
    738     if (selected.has(index)) {
    739       unselect(index, elem)
    740     } else {
    741       select(index, elem)
    742     }
    743     updateButtons()
    744   }
    745 
    746   function updateButtons() {
    747     const enable = (search.value != "" || selected.size != 0)
    748     if (buttonsEnabled == enable) return
    749     buttonsEnabled = enable
    750     for (const id of ["focus", "ignore", "hide", "show"]) {
    751       const link = document.getElementById(id)
    752       if (link != null) {
    753         link.classList.toggle("disabled", !enable)
    754       }
    755     }
    756   }
    757 
    758   // Initialize button states
    759   updateButtons()
    760 
    761   // Setup event handlers
    762   initMenus()
    763   if (svg != null) {
    764     initPanAndZoom(svg, toggleSvgSelect)
    765   }
    766   if (toptable != null) {
    767     toptable.addEventListener("mousedown", handleTopClick)
    768     toptable.addEventListener("touchstart", handleTopClick)
    769   }
    770 
    771   const ids = ["topbtn", "graphbtn", "peek", "list", "disasm",
    772                "focus", "ignore", "hide", "show"]
    773   ids.forEach(makeLinkDynamic)
    774 
    775   // Bind action to button with specified id.
    776   function addAction(id, action) {
    777     const btn = document.getElementById(id)
    778     if (btn != null) {
    779       btn.addEventListener("click", action)
    780       btn.addEventListener("touchstart", action)
    781     }
    782   }
    783 
    784   addAction("details", handleDetails)
    785   addAction("closedetails", handleCloseDetails)
    786 
    787   search.addEventListener("input", handleSearch)
    788   search.addEventListener("keydown", handleKey)
    789 
    790   // Give initial focus to main container so it can be scrolled using keys.
    791   const main = document.getElementById("bodycontainer")
    792   if (main) {
    793     main.focus()
    794   }
    795 }
    796 </script>
    797 {{end}}
    798 
    799 {{define "top" -}}
    800 <!DOCTYPE html>
    801 <html>
    802 <head>
    803 <meta charset="utf-8">
    804 <title>{{.Title}}</title>
    805 {{template "css" .}}
    806 <style type="text/css">
    807 </style>
    808 </head>
    809 <body>
    810 
    811 {{template "header" .}}
    812 
    813 <div id="bodycontainer">
    814 <table id="toptable">
    815 <tr>
    816 <th id="flathdr1">Flat
    817 <th id="flathdr2">Flat%
    818 <th>Sum%
    819 <th id="cumhdr1">Cum
    820 <th id="cumhdr2">Cum%
    821 <th id="namehdr">Name
    822 <th>Inlined?</tr>
    823 <tbody id="rows">
    824 </tbody>
    825 </table>
    826 </div>
    827 
    828 {{template "script" .}}
    829 <script>
    830 function makeTopTable(total, entries) {
    831   const rows = document.getElementById("rows")
    832   if (rows == null) return
    833 
    834   // Store initial index in each entry so we have stable node ids for selection.
    835   for (let i = 0; i < entries.length; i++) {
    836     entries[i].Id = "node" + i
    837   }
    838 
    839   // Which column are we currently sorted by and in what order?
    840   let currentColumn = ""
    841   let descending = false
    842   sortBy("Flat")
    843 
    844   function sortBy(column) {
    845     // Update sort criteria
    846     if (column == currentColumn) {
    847       descending = !descending  // Reverse order
    848     } else {
    849       currentColumn = column
    850       descending = (column != "Name")
    851     }
    852 
    853     // Sort according to current criteria.
    854     function cmp(a, b) {
    855       const av = a[currentColumn]
    856       const bv = b[currentColumn]
    857       if (av < bv) return -1
    858       if (av > bv) return +1
    859       return 0
    860     }
    861     entries.sort(cmp)
    862     if (descending) entries.reverse()
    863 
    864     function addCell(tr, val) {
    865       const td = document.createElement('td')
    866       td.textContent = val
    867       tr.appendChild(td)
    868     }
    869 
    870     function percent(v) {
    871       return (v * 100.0 / total).toFixed(2) + "%"
    872     }
    873 
    874     // Generate rows
    875     const fragment = document.createDocumentFragment()
    876     let sum = 0
    877     for (const row of entries) {
    878       const tr = document.createElement('tr')
    879       tr.id = row.Id
    880       sum += row.Flat
    881       addCell(tr, row.FlatFormat)
    882       addCell(tr, percent(row.Flat))
    883       addCell(tr, percent(sum))
    884       addCell(tr, row.CumFormat)
    885       addCell(tr, percent(row.Cum))
    886       addCell(tr, row.Name)
    887       addCell(tr, row.InlineLabel)
    888       fragment.appendChild(tr)
    889     }
    890 
    891     rows.textContent = ''  // Remove old rows
    892     rows.appendChild(fragment)
    893   }
    894 
    895   // Make different column headers trigger sorting.
    896   function bindSort(id, column) {
    897     const hdr = document.getElementById(id)
    898     if (hdr == null) return
    899     const fn = function() { sortBy(column) }
    900     hdr.addEventListener("click", fn)
    901     hdr.addEventListener("touch", fn)
    902   }
    903   bindSort("flathdr1", "Flat")
    904   bindSort("flathdr2", "Flat")
    905   bindSort("cumhdr1", "Cum")
    906   bindSort("cumhdr2", "Cum")
    907   bindSort("namehdr", "Name")
    908 }
    909 
    910 viewer({{.BaseURL}}, {{.Nodes}})
    911 makeTopTable({{.Total}}, {{.Top}})
    912 </script>
    913 </body>
    914 </html>
    915 {{end}}
    916 
    917 {{define "sourcelisting" -}}
    918 <!DOCTYPE html>
    919 <html>
    920 <head>
    921 <meta charset="utf-8">
    922 <title>{{.Title}}</title>
    923 {{template "css" .}}
    924 {{template "weblistcss" .}}
    925 {{template "weblistjs" .}}
    926 </head>
    927 <body>
    928 
    929 {{template "header" .}}
    930 
    931 <div id="bodycontainer">
    932 {{.HTMLBody}}
    933 </div>
    934 
    935 {{template "script" .}}
    936 <script>viewer({{.BaseURL}}, null)</script>
    937 </body>
    938 </html>
    939 {{end}}
    940 
    941 {{define "plaintext" -}}
    942 <!DOCTYPE html>
    943 <html>
    944 <head>
    945 <meta charset="utf-8">
    946 <title>{{.Title}}</title>
    947 {{template "css" .}}
    948 </head>
    949 <body>
    950 
    951 {{template "header" .}}
    952 
    953 <div id="bodycontainer">
    954 <pre>
    955 {{.TextBody}}
    956 </pre>
    957 </div>
    958 
    959 {{template "script" .}}
    960 <script>viewer({{.BaseURL}}, null)</script>
    961 </body>
    962 </html>
    963 {{end}}
    964 `))
    965 }
    966