web: Remove 'Update' button in static HTML GUI (#10220)#10349
Open
openroad-ci wants to merge 1 commit intoThe-OpenROAD-Project:masterfrom
Open
web: Remove 'Update' button in static HTML GUI (#10220)#10349openroad-ci wants to merge 1 commit intoThe-OpenROAD-Project:masterfrom
openroad-ci wants to merge 1 commit intoThe-OpenROAD-Project:masterfrom
Conversation
…10220) Signed-off-by: Jorge Ferreira <jorge.ferreira@precisioninno.com>
Contributor
|
clang-tidy review says "All clean, LGTM! 👍" |
Contributor
There was a problem hiding this comment.
Code Review
This pull request introduces a "static mode" for the ChartsWidget, ClockTreeWidget, HierarchyBrowser, and TimingWidget components, which hides the "Update" button, automates data fetching on initialization, and updates empty-state messages. The reviewer noted that the PR contains extensive unrelated formatting and indentation changes across all modified files and requested that these refactoring efforts be moved to a separate pull request to keep the functional changes focused.
| // Canvas-based clock tree viewer widget. | ||
|
|
||
| import { getThemeColors } from './theme.js'; | ||
| import {getThemeColors} from './theme.js'; |
Contributor
Comment on lines
+6
to
19
| import {CheckboxTreeModel} from './checkbox-tree-model.js'; | ||
| import {makeResizableHeaders} from './ui-utils.js'; | ||
|
|
||
| const COLS = [ | ||
| 'Instance', 'Module', 'Instances', 'Macros', 'Modules', | ||
| 'Area', 'Local Inst', 'Local Macros', 'Local Modules', | ||
| 'Instance', | ||
| 'Module', | ||
| 'Instances', | ||
| 'Macros', | ||
| 'Modules', | ||
| 'Area', | ||
| 'Local Inst', | ||
| 'Local Macros', | ||
| 'Local Modules', | ||
| ]; |
Contributor
There was a problem hiding this comment.
Comment on lines
+6
to
+407
| import {makeResizableHeaders} from './ui-utils.js'; | ||
|
|
||
| export class TimingWidget { | ||
| constructor(app, redrawAllLayers) { | ||
| this._app = app; | ||
| this._redrawAllLayers = redrawAllLayers; | ||
|
|
||
| this._currentTab = 'setup'; | ||
| this._setupPaths = []; | ||
| this._holdPaths = []; | ||
| this._selectedPathIndex = -1; | ||
| this._detailTab = 'data'; | ||
| this._selectedDetailIndex = -1; | ||
|
|
||
| this._build(); | ||
| constructor(app, redrawAllLayers) { | ||
| this._app = app; | ||
| this._redrawAllLayers = redrawAllLayers; | ||
|
|
||
| this._currentTab = 'setup'; | ||
| this._setupPaths = []; | ||
| this._holdPaths = []; | ||
| this._selectedPathIndex = -1; | ||
| this._detailTab = 'data'; | ||
| this._selectedDetailIndex = -1; | ||
|
|
||
| this._build(); | ||
| } | ||
|
|
||
| _build() { | ||
| const el = document.createElement('div'); | ||
| el.className = 'timing-widget'; | ||
|
|
||
| // --- Toolbar --- | ||
| const toolbar = document.createElement('div'); | ||
| toolbar.className = 'timing-toolbar'; | ||
|
|
||
| this._updateBtn = document.createElement('button'); | ||
| this._updateBtn.className = 'timing-btn'; | ||
| this._updateBtn.textContent = 'Update'; | ||
| if (this._app.websocketManager && this._app.websocketManager.isStaticMode) { | ||
| this._updateBtn.style.display = 'none'; | ||
| } | ||
|
|
||
| _build() { | ||
| const el = document.createElement('div'); | ||
| el.className = 'timing-widget'; | ||
|
|
||
| // --- Toolbar --- | ||
| const toolbar = document.createElement('div'); | ||
| toolbar.className = 'timing-toolbar'; | ||
|
|
||
| this._updateBtn = document.createElement('button'); | ||
| this._updateBtn.className = 'timing-btn'; | ||
| this._updateBtn.textContent = 'Update'; | ||
|
|
||
| this._pathCountLabel = document.createElement('span'); | ||
| this._pathCountLabel.className = 'timing-path-count'; | ||
|
|
||
| toolbar.appendChild(this._updateBtn); | ||
| toolbar.appendChild(this._pathCountLabel); | ||
| el.appendChild(toolbar); | ||
|
|
||
| // --- Setup/Hold Tab Bar --- | ||
| const tabBar = document.createElement('div'); | ||
| tabBar.className = 'timing-tab-bar'; | ||
|
|
||
| this._setupTab = this._makeTab('Setup', true); | ||
| this._holdTab = this._makeTab('Hold', false); | ||
| tabBar.appendChild(this._setupTab); | ||
| tabBar.appendChild(this._holdTab); | ||
| el.appendChild(tabBar); | ||
|
|
||
| // --- Path listing table --- | ||
| this._pathTableContainer = document.createElement('div'); | ||
| this._pathTableContainer.className = 'timing-path-table-container'; | ||
| this._pathTable = document.createElement('table'); | ||
| this._pathTable.className = 'timing-table'; | ||
| this._pathTableContainer.appendChild(this._pathTable); | ||
| el.appendChild(this._pathTableContainer); | ||
|
|
||
| // --- Detail Tab Bar --- | ||
| const detailTabBar = document.createElement('div'); | ||
| detailTabBar.className = 'timing-tab-bar'; | ||
| this._dataTab = this._makeTab('Data Path', true); | ||
| this._captureTab = this._makeTab('Capture Path', false); | ||
| detailTabBar.appendChild(this._dataTab); | ||
| detailTabBar.appendChild(this._captureTab); | ||
| el.appendChild(detailTabBar); | ||
|
|
||
| // --- Detail table --- | ||
| this._detailTableContainer = document.createElement('div'); | ||
| this._detailTableContainer.className = 'timing-detail-table-container'; | ||
| this._detailTable = document.createElement('table'); | ||
| this._detailTable.className = 'timing-table'; | ||
| this._detailTableContainer.appendChild(this._detailTable); | ||
| el.appendChild(this._detailTableContainer); | ||
|
|
||
| this.element = el; | ||
|
|
||
| this._bindEvents(); | ||
| this._pathCountLabel = document.createElement('span'); | ||
| this._pathCountLabel.className = 'timing-path-count'; | ||
|
|
||
| toolbar.appendChild(this._updateBtn); | ||
| toolbar.appendChild(this._pathCountLabel); | ||
| el.appendChild(toolbar); | ||
|
|
||
| // --- Setup/Hold Tab Bar --- | ||
| const tabBar = document.createElement('div'); | ||
| tabBar.className = 'timing-tab-bar'; | ||
|
|
||
| this._setupTab = this._makeTab('Setup', true); | ||
| this._holdTab = this._makeTab('Hold', false); | ||
| tabBar.appendChild(this._setupTab); | ||
| tabBar.appendChild(this._holdTab); | ||
| el.appendChild(tabBar); | ||
|
|
||
| // --- Path listing table --- | ||
| this._pathTableContainer = document.createElement('div'); | ||
| this._pathTableContainer.className = 'timing-path-table-container'; | ||
| this._pathTable = document.createElement('table'); | ||
| this._pathTable.className = 'timing-table'; | ||
| this._pathTableContainer.appendChild(this._pathTable); | ||
| el.appendChild(this._pathTableContainer); | ||
|
|
||
| // --- Detail Tab Bar --- | ||
| const detailTabBar = document.createElement('div'); | ||
| detailTabBar.className = 'timing-tab-bar'; | ||
| this._dataTab = this._makeTab('Data Path', true); | ||
| this._captureTab = this._makeTab('Capture Path', false); | ||
| detailTabBar.appendChild(this._dataTab); | ||
| detailTabBar.appendChild(this._captureTab); | ||
| el.appendChild(detailTabBar); | ||
|
|
||
| // --- Detail table --- | ||
| this._detailTableContainer = document.createElement('div'); | ||
| this._detailTableContainer.className = 'timing-detail-table-container'; | ||
| this._detailTable = document.createElement('table'); | ||
| this._detailTable.className = 'timing-table'; | ||
| this._detailTableContainer.appendChild(this._detailTable); | ||
| el.appendChild(this._detailTableContainer); | ||
|
|
||
| this.element = el; | ||
|
|
||
| this._bindEvents(); | ||
|
|
||
| if (this._app.websocketManager && this._app.websocketManager.isStaticMode) { | ||
| setTimeout(() => this.update(), 0); | ||
| } | ||
|
|
||
| _makeTab(label, active) { | ||
| const btn = document.createElement('button'); | ||
| btn.className = 'timing-tab' + (active ? ' active' : ''); | ||
| btn.textContent = label; | ||
| return btn; | ||
| } | ||
|
|
||
| _bindEvents() { | ||
| // Tab switching | ||
| this._setupTab.addEventListener('click', () => { | ||
| this._currentTab = 'setup'; | ||
| this._setupTab.classList.add('active'); | ||
| this._holdTab.classList.remove('active'); | ||
| this._selectedPathIndex = -1; | ||
| this._renderPathTable(); | ||
| this._renderDetailTable(); | ||
| this._clearTimingHighlight(); | ||
| }); | ||
| this._holdTab.addEventListener('click', () => { | ||
| this._currentTab = 'hold'; | ||
| this._holdTab.classList.add('active'); | ||
| this._setupTab.classList.remove('active'); | ||
| this._selectedPathIndex = -1; | ||
| this._renderPathTable(); | ||
| this._renderDetailTable(); | ||
| this._clearTimingHighlight(); | ||
| }); | ||
| this._dataTab.addEventListener('click', () => { | ||
| this._detailTab = 'data'; | ||
| this._dataTab.classList.add('active'); | ||
| this._captureTab.classList.remove('active'); | ||
| this._renderDetailTable(); | ||
| }); | ||
| this._captureTab.addEventListener('click', () => { | ||
| this._detailTab = 'capture'; | ||
| this._captureTab.classList.add('active'); | ||
| this._dataTab.classList.remove('active'); | ||
| this._renderDetailTable(); | ||
| }); | ||
|
|
||
| // Fetch paths | ||
| this._updateBtn.addEventListener('click', () => this.update()); | ||
|
|
||
| // Keyboard navigation — path table | ||
| this._pathTableContainer.setAttribute('tabindex', '0'); | ||
| this._pathTableContainer.style.outline = 'none'; | ||
| this._pathTableContainer.addEventListener('keydown', (e) => { | ||
| if (e.key === 'ArrowDown') { | ||
| e.preventDefault(); | ||
| const rows = this._pathTable.querySelectorAll('tbody tr'); | ||
| if (this._selectedPathIndex < rows.length - 1) { | ||
| this._selectPathRow(this._selectedPathIndex + 1); | ||
| } | ||
| } else if (e.key === 'ArrowUp') { | ||
| e.preventDefault(); | ||
| if (this._selectedPathIndex > 0) { | ||
| this._selectPathRow(this._selectedPathIndex - 1); | ||
| } | ||
| } | ||
| }); | ||
|
|
||
| // Keyboard navigation — detail table | ||
| this._detailTableContainer.setAttribute('tabindex', '0'); | ||
| this._detailTableContainer.style.outline = 'none'; | ||
| this._detailTableContainer.addEventListener('keydown', (e) => { | ||
| if (e.key === 'ArrowDown') { | ||
| e.preventDefault(); | ||
| const rows = this._detailTable.querySelectorAll('tbody tr'); | ||
| if (this._selectedDetailIndex < rows.length - 1) { | ||
| this._selectDetailRow(this._selectedDetailIndex + 1); | ||
| } | ||
| } else if (e.key === 'ArrowUp') { | ||
| e.preventDefault(); | ||
| if (this._selectedDetailIndex > 0) { | ||
| this._selectDetailRow(this._selectedDetailIndex - 1); | ||
| } | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| showPaths(tab, paths) { | ||
| this._currentTab = tab; | ||
| if (tab === 'setup') { | ||
| this._setupPaths = paths; | ||
| this._setupTab.classList.add('active'); | ||
| this._holdTab.classList.remove('active'); | ||
| } else { | ||
| this._holdPaths = paths; | ||
| this._holdTab.classList.add('active'); | ||
| this._setupTab.classList.remove('active'); | ||
| } | ||
|
|
||
| _makeTab(label, active) { | ||
| const btn = document.createElement('button'); | ||
| btn.className = 'timing-tab' + (active ? ' active' : ''); | ||
| btn.textContent = label; | ||
| return btn; | ||
| } | ||
|
|
||
| _bindEvents() { | ||
| // Tab switching | ||
| this._setupTab.addEventListener('click', () => { | ||
| this._currentTab = 'setup'; | ||
| this._setupTab.classList.add('active'); | ||
| this._holdTab.classList.remove('active'); | ||
| this._selectedPathIndex = -1; | ||
| this._renderPathTable(); | ||
| this._renderDetailTable(); | ||
| this._clearTimingHighlight(); | ||
| }); | ||
| this._holdTab.addEventListener('click', () => { | ||
| this._currentTab = 'hold'; | ||
| this._holdTab.classList.add('active'); | ||
| this._setupTab.classList.remove('active'); | ||
| this._selectedPathIndex = -1; | ||
| this._renderPathTable(); | ||
| this._renderDetailTable(); | ||
| this._clearTimingHighlight(); | ||
| }); | ||
| this._dataTab.addEventListener('click', () => { | ||
| this._detailTab = 'data'; | ||
| this._dataTab.classList.add('active'); | ||
| this._captureTab.classList.remove('active'); | ||
| this._renderDetailTable(); | ||
| }); | ||
| this._captureTab.addEventListener('click', () => { | ||
| this._detailTab = 'capture'; | ||
| this._captureTab.classList.add('active'); | ||
| this._dataTab.classList.remove('active'); | ||
| this._renderDetailTable(); | ||
| }); | ||
|
|
||
| // Fetch paths | ||
| this._updateBtn.addEventListener('click', () => this.update()); | ||
|
|
||
| // Keyboard navigation — path table | ||
| this._pathTableContainer.setAttribute('tabindex', '0'); | ||
| this._pathTableContainer.style.outline = 'none'; | ||
| this._pathTableContainer.addEventListener('keydown', (e) => { | ||
| if (e.key === 'ArrowDown') { | ||
| e.preventDefault(); | ||
| const rows = this._pathTable.querySelectorAll('tbody tr'); | ||
| if (this._selectedPathIndex < rows.length - 1) { | ||
| this._selectPathRow(this._selectedPathIndex + 1); | ||
| } | ||
| this._selectedPathIndex = -1; | ||
| this._pathCountLabel.textContent = paths.length + ' paths'; | ||
| this._renderPathTable(); | ||
| this._renderDetailTable(); | ||
| this._clearTimingHighlight(); | ||
| } | ||
|
|
||
| async update() { | ||
| this._updateBtn.disabled = true; | ||
| this._updateBtn.textContent = 'Loading...'; | ||
| try { | ||
| const [setupData, holdData] = await Promise.all([ | ||
| this._app.websocketManager.request({ type: 'timing_report', is_setup: true, max_paths: 100 }), | ||
| this._app.websocketManager.request({ type: 'timing_report', is_setup: false, max_paths: 100 }), | ||
| ]); | ||
| this._setupPaths = setupData.paths || []; | ||
| this._holdPaths = holdData.paths || []; | ||
| this._selectedPathIndex = -1; | ||
| this._renderPathTable(); | ||
| this._renderDetailTable(); | ||
| this._clearTimingHighlight(); | ||
| } catch (e) { | ||
| console.error('Timing fetch failed:', e); | ||
| } else if (e.key === 'ArrowUp') { | ||
| e.preventDefault(); | ||
| if (this._selectedPathIndex > 0) { | ||
| this._selectPathRow(this._selectedPathIndex - 1); | ||
| } | ||
| } | ||
| }); | ||
|
|
||
| // Keyboard navigation — detail table | ||
| this._detailTableContainer.setAttribute('tabindex', '0'); | ||
| this._detailTableContainer.style.outline = 'none'; | ||
| this._detailTableContainer.addEventListener('keydown', (e) => { | ||
| if (e.key === 'ArrowDown') { | ||
| e.preventDefault(); | ||
| const rows = this._detailTable.querySelectorAll('tbody tr'); | ||
| if (this._selectedDetailIndex < rows.length - 1) { | ||
| this._selectDetailRow(this._selectedDetailIndex + 1); | ||
| } | ||
| } else if (e.key === 'ArrowUp') { | ||
| e.preventDefault(); | ||
| if (this._selectedDetailIndex > 0) { | ||
| this._selectDetailRow(this._selectedDetailIndex - 1); | ||
| } | ||
| this._updateBtn.disabled = false; | ||
| this._updateBtn.textContent = 'Update'; | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| showPaths(tab, paths) { | ||
| this._currentTab = tab; | ||
| if (tab === 'setup') { | ||
| this._setupPaths = paths; | ||
| this._setupTab.classList.add('active'); | ||
| this._holdTab.classList.remove('active'); | ||
| } else { | ||
| this._holdPaths = paths; | ||
| this._holdTab.classList.add('active'); | ||
| this._setupTab.classList.remove('active'); | ||
| } | ||
|
|
||
| _clearTimingHighlight() { | ||
| this._app.websocketManager.request({ type: 'timing_highlight', path_index: -1 }) | ||
| .then(() => this._redrawAllLayers()); | ||
| this._selectedPathIndex = -1; | ||
| this._pathCountLabel.textContent = paths.length + ' paths'; | ||
| this._renderPathTable(); | ||
| this._renderDetailTable(); | ||
| this._clearTimingHighlight(); | ||
| } | ||
|
|
||
| async update() { | ||
| this._updateBtn.disabled = true; | ||
| this._updateBtn.textContent = 'Loading...'; | ||
| try { | ||
| const [setupData, holdData] = await Promise.all([ | ||
| this._app.websocketManager.request( | ||
| {type: 'timing_report', is_setup: true, max_paths: 100}), | ||
| this._app.websocketManager.request( | ||
| {type: 'timing_report', is_setup: false, max_paths: 100}), | ||
| ]); | ||
| this._setupPaths = setupData.paths || []; | ||
| this._holdPaths = holdData.paths || []; | ||
| this._selectedPathIndex = -1; | ||
| this._renderPathTable(); | ||
| this._renderDetailTable(); | ||
| this._clearTimingHighlight(); | ||
| } catch (e) { | ||
| console.error('Timing fetch failed:', e); | ||
| } | ||
|
|
||
| _selectPathRow(idx) { | ||
| const rows = this._pathTable.querySelectorAll('tbody tr'); | ||
| if (idx < 0 || idx >= rows.length) return; | ||
| this._selectedPathIndex = idx; | ||
| for (const row of rows) row.classList.remove('selected'); | ||
| rows[idx].classList.add('selected'); | ||
| rows[idx].scrollIntoView({ block: 'nearest' }); | ||
| this._pathTableContainer.focus(); | ||
| this._renderDetailTable(); | ||
| // Use _originalIndex when paths were filtered (e.g. by histogram | ||
| // column click in static mode) so the overlay lookup matches. | ||
| const paths = this._currentTab === 'setup' ? this._setupPaths : this._holdPaths; | ||
| const highlightIdx = paths[idx]?._originalIndex ?? idx; | ||
| this._app.websocketManager.request({ | ||
| type: 'timing_highlight', | ||
| path_index: highlightIdx, | ||
| is_setup: this._currentTab === 'setup', | ||
| }).then(() => this._redrawAllLayers()) | ||
| .catch(err => console.error('timing_highlight error:', err)); | ||
| this._updateBtn.disabled = false; | ||
| this._updateBtn.textContent = 'Update'; | ||
| } | ||
|
|
||
| _clearTimingHighlight() { | ||
| this._app.websocketManager | ||
| .request({type: 'timing_highlight', path_index: -1}) | ||
| .then(() => this._redrawAllLayers()); | ||
| } | ||
|
|
||
| _selectPathRow(idx) { | ||
| const rows = this._pathTable.querySelectorAll('tbody tr'); | ||
| if (idx < 0 || idx >= rows.length) return; | ||
| this._selectedPathIndex = idx; | ||
| for (const row of rows) row.classList.remove('selected'); | ||
| rows[idx].classList.add('selected'); | ||
| rows[idx].scrollIntoView({block: 'nearest'}); | ||
| this._pathTableContainer.focus(); | ||
| this._renderDetailTable(); | ||
| // Use _originalIndex when paths were filtered (e.g. by histogram | ||
| // column click in static mode) so the overlay lookup matches. | ||
| const paths = | ||
| this._currentTab === 'setup' ? this._setupPaths : this._holdPaths; | ||
| const highlightIdx = paths[idx]?._originalIndex ?? idx; | ||
| this._app.websocketManager | ||
| .request({ | ||
| type: 'timing_highlight', | ||
| path_index: highlightIdx, | ||
| is_setup: this._currentTab === 'setup', | ||
| }) | ||
| .then(() => this._redrawAllLayers()) | ||
| .catch(err => console.error('timing_highlight error:', err)); | ||
| } | ||
|
|
||
| _renderPathTable() { | ||
| const paths = | ||
| this._currentTab === 'setup' ? this._setupPaths : this._holdPaths; | ||
| this._pathCountLabel.textContent = paths.length + ' paths'; | ||
|
|
||
| // Preserve column widths across re-renders. | ||
| const oldHeaders = this._pathTable.querySelectorAll('thead th'); | ||
| const savedWidths = Array.from(oldHeaders, th => th.style.width); | ||
|
|
||
| this._pathTable.innerHTML = ''; | ||
|
|
||
| const thead = document.createElement('thead'); | ||
| const hr = document.createElement('tr'); | ||
| for (const col of TimingWidget.PATH_COLS) { | ||
| const th = document.createElement('th'); | ||
| th.textContent = col; | ||
| hr.appendChild(th); | ||
| } | ||
|
|
||
| _renderPathTable() { | ||
| const paths = this._currentTab === 'setup' ? this._setupPaths : this._holdPaths; | ||
| this._pathCountLabel.textContent = paths.length + ' paths'; | ||
|
|
||
| // Preserve column widths across re-renders. | ||
| const oldHeaders = this._pathTable.querySelectorAll('thead th'); | ||
| const savedWidths = Array.from(oldHeaders, th => th.style.width); | ||
|
|
||
| this._pathTable.innerHTML = ''; | ||
|
|
||
| const thead = document.createElement('thead'); | ||
| const hr = document.createElement('tr'); | ||
| for (const col of TimingWidget.PATH_COLS) { | ||
| const th = document.createElement('th'); | ||
| th.textContent = col; | ||
| hr.appendChild(th); | ||
| } | ||
| thead.appendChild(hr); | ||
| this._pathTable.appendChild(thead); | ||
|
|
||
| const tbody = document.createElement('tbody'); | ||
| paths.forEach((p, idx) => { | ||
| const tr = document.createElement('tr'); | ||
| if (idx === this._selectedPathIndex) tr.classList.add('selected'); | ||
| const vals = [ | ||
| p.end_clk, | ||
| fmtTime(p.required), | ||
| fmtTime(p.arrival), | ||
| fmtTime(p.slack), | ||
| fmtTime(p.skew), | ||
| fmtTime(p.path_delay), | ||
| p.logic_depth, | ||
| p.fanout, | ||
| p.start_pin, | ||
| p.end_pin, | ||
| ]; | ||
| vals.forEach((v, ci) => { | ||
| const td = document.createElement('td'); | ||
| td.textContent = v; | ||
| if (ci === 3 && p.slack < 0) td.classList.add('slack-negative'); | ||
| tr.appendChild(td); | ||
| }); | ||
| tr.style.cursor = 'pointer'; | ||
| tr.addEventListener('click', () => this._selectPathRow(idx)); | ||
| tbody.appendChild(tr); | ||
| }); | ||
| this._pathTable.appendChild(tbody); | ||
|
|
||
| // Restore previous widths if available, otherwise compute fresh. | ||
| if (savedWidths.length > 0 && savedWidths[0]) { | ||
| const newHeaders = this._pathTable.querySelectorAll('thead th'); | ||
| this._pathTable.style.tableLayout = 'fixed'; | ||
| newHeaders.forEach((th, i) => { | ||
| if (i < savedWidths.length) th.style.width = savedWidths[i]; | ||
| }); | ||
| } else { | ||
| makeResizableHeaders(this._pathTable); | ||
| } | ||
| thead.appendChild(hr); | ||
| this._pathTable.appendChild(thead); | ||
|
|
||
| const tbody = document.createElement('tbody'); | ||
| paths.forEach((p, idx) => { | ||
| const tr = document.createElement('tr'); | ||
| if (idx === this._selectedPathIndex) tr.classList.add('selected'); | ||
| const vals = [ | ||
| p.end_clk, | ||
| fmtTime(p.required), | ||
| fmtTime(p.arrival), | ||
| fmtTime(p.slack), | ||
| fmtTime(p.skew), | ||
| fmtTime(p.path_delay), | ||
| p.logic_depth, | ||
| p.fanout, | ||
| p.start_pin, | ||
| p.end_pin, | ||
| ]; | ||
| vals.forEach((v, ci) => { | ||
| const td = document.createElement('td'); | ||
| td.textContent = v; | ||
| if (ci === 3 && p.slack < 0) td.classList.add('slack-negative'); | ||
| tr.appendChild(td); | ||
| }); | ||
| tr.style.cursor = 'pointer'; | ||
| tr.addEventListener('click', () => this._selectPathRow(idx)); | ||
| tbody.appendChild(tr); | ||
| }); | ||
| this._pathTable.appendChild(tbody); | ||
|
|
||
| // Restore previous widths if available, otherwise compute fresh. | ||
| if (savedWidths.length > 0 && savedWidths[0]) { | ||
| const newHeaders = this._pathTable.querySelectorAll('thead th'); | ||
| this._pathTable.style.tableLayout = 'fixed'; | ||
| newHeaders.forEach((th, i) => { | ||
| if (i < savedWidths.length) th.style.width = savedWidths[i]; | ||
| }); | ||
| } else { | ||
| makeResizableHeaders(this._pathTable); | ||
| } | ||
|
|
||
| _selectDetailRow(idx) { | ||
| const rows = this._detailTable.querySelectorAll('tbody tr'); | ||
| if (idx < 0 || idx >= rows.length) return; | ||
| this._selectedDetailIndex = idx; | ||
| for (const row of rows) { | ||
| row.classList.remove('timing-selected-row'); | ||
| } | ||
| rows[idx].classList.add('timing-selected-row'); | ||
| rows[idx].scrollIntoView({ block: 'nearest' }); | ||
| this._detailTableContainer.focus(); | ||
|
|
||
| const paths = this._currentTab === 'setup' ? this._setupPaths : this._holdPaths; | ||
| const path = paths[this._selectedPathIndex]; | ||
| const nodes = this._detailTab === 'data' ? path.data_nodes : path.capture_nodes; | ||
| // Use _originalIndex when paths were filtered (e.g. by histogram | ||
| // column click in static mode) so the overlay lookup matches. | ||
| const highlightIdx = path._originalIndex ?? this._selectedPathIndex; | ||
| this._app.websocketManager.request({ | ||
| type: 'timing_highlight', | ||
| path_index: highlightIdx, | ||
| is_setup: this._currentTab === 'setup', | ||
| pin_name: nodes[idx].pin, | ||
| }).then(() => this._redrawAllLayers()); | ||
| } | ||
|
|
||
| _selectDetailRow(idx) { | ||
| const rows = this._detailTable.querySelectorAll('tbody tr'); | ||
| if (idx < 0 || idx >= rows.length) return; | ||
| this._selectedDetailIndex = idx; | ||
| for (const row of rows) { | ||
| row.classList.remove('timing-selected-row'); | ||
| } | ||
|
|
||
| _renderDetailTable() { | ||
| // Preserve column widths across re-renders. | ||
| const oldHeaders = this._detailTable.querySelectorAll('thead th'); | ||
| const savedWidths = Array.from(oldHeaders, th => th.style.width); | ||
|
|
||
| this._detailTable.innerHTML = ''; | ||
| this._selectedDetailIndex = -1; | ||
| const paths = this._currentTab === 'setup' ? this._setupPaths : this._holdPaths; | ||
| if (this._selectedPathIndex < 0 || this._selectedPathIndex >= paths.length) return; | ||
|
|
||
| const path = paths[this._selectedPathIndex]; | ||
| const nodes = this._detailTab === 'data' ? path.data_nodes : path.capture_nodes; | ||
|
|
||
| const thead = document.createElement('thead'); | ||
| const hr = document.createElement('tr'); | ||
| for (const col of TimingWidget.DETAIL_COLS) { | ||
| const th = document.createElement('th'); | ||
| th.textContent = col; | ||
| hr.appendChild(th); | ||
| } | ||
| thead.appendChild(hr); | ||
| this._detailTable.appendChild(thead); | ||
|
|
||
| const tbody = document.createElement('tbody'); | ||
| nodes.forEach((n, idx) => { | ||
| const tr = document.createElement('tr'); | ||
| if (n.clk) tr.classList.add('timing-clock-row'); | ||
| const vals = [ | ||
| n.pin, | ||
| n.fanout, | ||
| n.rise ? '↑' : '↓', | ||
| fmtTime(n.time), | ||
| fmtTime(n.delay), | ||
| fmtTime(n.slew), | ||
| fmtTime(n.load), | ||
| ]; | ||
| for (const v of vals) { | ||
| const td = document.createElement('td'); | ||
| td.textContent = v; | ||
| tr.appendChild(td); | ||
| } | ||
| tr.style.cursor = 'pointer'; | ||
| tr.addEventListener('click', () => this._selectDetailRow(idx)); | ||
| tbody.appendChild(tr); | ||
| }); | ||
| this._detailTable.appendChild(tbody); | ||
|
|
||
| if (savedWidths.length > 0 && savedWidths[0]) { | ||
| const newHeaders = this._detailTable.querySelectorAll('thead th'); | ||
| this._detailTable.style.tableLayout = 'fixed'; | ||
| newHeaders.forEach((th, i) => { | ||
| if (i < savedWidths.length) th.style.width = savedWidths[i]; | ||
| }); | ||
| } else { | ||
| makeResizableHeaders(this._detailTable); | ||
| // Pin column: set initial width to 30 characters | ||
| const pinTh = this._detailTable.querySelector('thead th'); | ||
| if (pinTh) { | ||
| pinTh.style.width = '30ch'; | ||
| } | ||
| } | ||
| rows[idx].classList.add('timing-selected-row'); | ||
| rows[idx].scrollIntoView({block: 'nearest'}); | ||
| this._detailTableContainer.focus(); | ||
|
|
||
| const paths = | ||
| this._currentTab === 'setup' ? this._setupPaths : this._holdPaths; | ||
| const path = paths[this._selectedPathIndex]; | ||
| const nodes = | ||
| this._detailTab === 'data' ? path.data_nodes : path.capture_nodes; | ||
| // Use _originalIndex when paths were filtered (e.g. by histogram | ||
| // column click in static mode) so the overlay lookup matches. | ||
| const highlightIdx = path._originalIndex ?? this._selectedPathIndex; | ||
| this._app.websocketManager | ||
| .request({ | ||
| type: 'timing_highlight', | ||
| path_index: highlightIdx, | ||
| is_setup: this._currentTab === 'setup', | ||
| pin_name: nodes[idx].pin, | ||
| }) | ||
| .then(() => this._redrawAllLayers()); | ||
| } | ||
|
|
||
| _renderDetailTable() { | ||
| // Preserve column widths across re-renders. | ||
| const oldHeaders = this._detailTable.querySelectorAll('thead th'); | ||
| const savedWidths = Array.from(oldHeaders, th => th.style.width); | ||
|
|
||
| this._detailTable.innerHTML = ''; | ||
| this._selectedDetailIndex = -1; | ||
| const paths = | ||
| this._currentTab === 'setup' ? this._setupPaths : this._holdPaths; | ||
| if (this._selectedPathIndex < 0 || this._selectedPathIndex >= paths.length) | ||
| return; | ||
|
|
||
| const path = paths[this._selectedPathIndex]; | ||
| const nodes = | ||
| this._detailTab === 'data' ? path.data_nodes : path.capture_nodes; | ||
|
|
||
| const thead = document.createElement('thead'); | ||
| const hr = document.createElement('tr'); | ||
| for (const col of TimingWidget.DETAIL_COLS) { | ||
| const th = document.createElement('th'); | ||
| th.textContent = col; | ||
| hr.appendChild(th); | ||
| } | ||
|
|
||
| thead.appendChild(hr); | ||
| this._detailTable.appendChild(thead); | ||
|
|
||
| const tbody = document.createElement('tbody'); | ||
| nodes.forEach((n, idx) => { | ||
| const tr = document.createElement('tr'); | ||
| if (n.clk) tr.classList.add('timing-clock-row'); | ||
| const vals = [ | ||
| n.pin, | ||
| n.fanout, | ||
| n.rise ? '↑' : '↓', | ||
| fmtTime(n.time), | ||
| fmtTime(n.delay), | ||
| fmtTime(n.slew), | ||
| fmtTime(n.load), | ||
| ]; | ||
| for (const v of vals) { | ||
| const td = document.createElement('td'); | ||
| td.textContent = v; | ||
| tr.appendChild(td); | ||
| } | ||
| tr.style.cursor = 'pointer'; | ||
| tr.addEventListener('click', () => this._selectDetailRow(idx)); | ||
| tbody.appendChild(tr); | ||
| }); | ||
| this._detailTable.appendChild(tbody); | ||
|
|
||
| if (savedWidths.length > 0 && savedWidths[0]) { | ||
| const newHeaders = this._detailTable.querySelectorAll('thead th'); | ||
| this._detailTable.style.tableLayout = 'fixed'; | ||
| newHeaders.forEach((th, i) => { | ||
| if (i < savedWidths.length) th.style.width = savedWidths[i]; | ||
| }); | ||
| } else { | ||
| makeResizableHeaders(this._detailTable); | ||
| // Pin column: set initial width to 30 characters | ||
| const pinTh = this._detailTable.querySelector('thead th'); | ||
| if (pinTh) { | ||
| pinTh.style.width = '30ch'; | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| export function fmtTime(v) { | ||
| if (v === undefined || v === null) return ''; | ||
| return typeof v === 'number' ? v.toFixed(4) : String(v); | ||
| if (v === undefined || v === null) return ''; | ||
| return typeof v === 'number' ? v.toFixed(4) : String(v); | ||
| } | ||
|
|
||
| TimingWidget.PATH_COLS = ['Clock', 'Required', 'Arrival', 'Slack', 'Skew', | ||
| 'Logic Delay', 'Logic Depth', 'Fanout', 'Start', 'End']; | ||
| TimingWidget.DETAIL_COLS = ['Pin', 'Fanout', 'R/F', 'Time', 'Delay', 'Slew', 'Load']; | ||
| TimingWidget.PATH_COLS = [ | ||
| 'Clock', 'Required', 'Arrival', 'Slack', 'Skew', 'Logic Delay', 'Logic Depth', | ||
| 'Fanout', 'Start', 'End' | ||
| ]; | ||
| TimingWidget.DETAIL_COLS = | ||
| ['Pin', 'Fanout', 'R/F', 'Time', 'Delay', 'Slew', 'Load']; |
Contributor
There was a problem hiding this comment.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
fix #10220