Skip to content

web: Componentize layout view for web - #11806

Open
QuantamHD wants to merge 1 commit into
The-OpenROAD-Project:masterfrom
QuantamHD:fix_ide_panes
Open

QuantamHD wants to merge 1 commit into
The-OpenROAD-Project:masterfrom
QuantamHD:fix_ide_panes

Conversation

@QuantamHD

Copy link
Copy Markdown
Collaborator

This fixes a number of layout bugs I found in the web viewer.

I moved its implementation to web components and css grid, which should make it significantly more stable.

@QuantamHD
QuantamHD requested a review from a team as a code owner October 17, 2026 07:48
@QuantamHD
QuantamHD requested a review from gadfort October 17, 2026 07:48
@github-actions

Copy link
Copy Markdown
Contributor

This PR exceeds the recommended size of 1000 lines. Please make sure you are NOT addressing multiple issues with one PR. Note this PR might be rejected due to its size.

This fixes a number of layout bugs I found in the web viewer.

I moved its implementation to web components and css grid, which
should make it significantly more stable.

Signed-off-by: Ethan Mahintorabi <ethanmoon@google.com>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request replaces the external GoldenLayout dependency with a custom, native CSS Grid-based layout manager (GridLayoutManager) along with custom elements (, , and ) to handle window panes, drag-to-resize splitters, and tabs. It also updates the main application, styles, and adds comprehensive unit tests. The review feedback highlights several areas for improvement: adding truthiness checks for the global app object to prevent potential TypeError crashes, reusing restorePane and collapsePane helper methods to ensure UI states and local storage persistence remain synchronized, and binding pointer drag event listeners to window instead of the narrow splitter elements to prevent drag operations from breaking.

Comment on lines +899 to +901
if (typeof app !== 'undefined' && app.map) {
setTimeout(() => app.map.invalidateSize({ animate: false }), 0);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

If app is null, typeof app !== 'undefined' evaluates to true (since typeof null is 'object'). Accessing app.map will then throw a TypeError. Adding a truthiness check for app ensures robust defensive programming.

Suggested change
if (typeof app !== 'undefined' && app.map) {
setTimeout(() => app.map.invalidateSize({ animate: false }), 0);
}
if (typeof app !== 'undefined' && app && app.map) {
setTimeout(() => app.map.invalidateSize({ animate: false }), 0);
}

Comment on lines +1080 to +1087
if (typeof app !== 'undefined') {
if (componentType === 'LayoutViewer' && app.map) {
setTimeout(() => app.map.invalidateSize({ animate: false }), 0);
}
if (componentType === '3DViewer' && app.threeDViewerWidget) {
setTimeout(() => app.threeDViewerWidget.onOpen(), 0);
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

If app is null, typeof app !== 'undefined' evaluates to true, and accessing app.map or app.threeDViewerWidget will throw a TypeError. Adding a truthiness check for app prevents potential runtime crashes.

Suggested change
if (typeof app !== 'undefined') {
if (componentType === 'LayoutViewer' && app.map) {
setTimeout(() => app.map.invalidateSize({ animate: false }), 0);
}
if (componentType === '3DViewer' && app.threeDViewerWidget) {
setTimeout(() => app.threeDViewerWidget.onOpen(), 0);
}
}
if (typeof app !== 'undefined' && app) {
if (componentType === 'LayoutViewer' && app.map) {
setTimeout(() => app.map.invalidateSize({ animate: false }), 0);
}
if (componentType === '3DViewer' && app.threeDViewerWidget) {
setTimeout(() => app.threeDViewerWidget.onOpen(), 0);
}
}

Comment on lines +1043 to +1055
if (comp.paneName === 'left') {
this.containerEl.classList.remove('hide-left');
const btn = this.containerEl.querySelector('#btn-collapse-left');
if (btn) { btn.textContent = '◀'; btn.title = 'Collapse Left Panel'; }
} else if (comp.paneName === 'right') {
this.containerEl.classList.remove('hide-right');
const btn = this.containerEl.querySelector('#btn-collapse-right');
if (btn) { btn.textContent = '▶'; btn.title = 'Collapse Right Panel'; }
} else if (comp.paneName === 'bottom' && this.centerEl) {
this.centerEl.classList.remove('hide-bottom');
const btn = this.containerEl.querySelector('#btn-collapse-bottom');
if (btn) { btn.textContent = '▼'; btn.title = 'Collapse Console'; }
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

This block manually removes the hide-* classes and updates the collapse buttons, which duplicates the logic in restorePane and _setPaneCollapsed. Crucially, it misses calling setCollapsed(false) on the pane component and saveLayout(), causing the custom element's internal state to desynchronize and the uncollapsed state to not be persisted. Reusing restorePane resolves these issues cleanly.

        if (['left', 'right', 'bottom'].includes(comp.paneName)) {
            this.restorePane(comp.paneName);
        }

Comment on lines +1121 to +1125
this.activeTabs[comp.paneName] = null;
if (comp.paneName === 'left') this.containerEl.classList.add('hide-left');
if (comp.paneName === 'right') this.containerEl.classList.add('hide-right');
if (comp.paneName === 'bottom' && this.centerEl) this.centerEl.classList.add('hide-bottom');
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Manually adding the hide-* classes here duplicates the collapsing logic and bypasses collapsePane. This results in the custom element's internal state and collapse button icon/title not being updated, and the collapsed state not being persisted to localStorage. Reusing collapsePane ensures consistent state and proper persistence.

Suggested change
this.activeTabs[comp.paneName] = null;
if (comp.paneName === 'left') this.containerEl.classList.add('hide-left');
if (comp.paneName === 'right') this.containerEl.classList.add('hide-right');
if (comp.paneName === 'bottom' && this.centerEl) this.centerEl.classList.add('hide-bottom');
}
this.activeTabs[comp.paneName] = null;
if (['left', 'right', 'bottom'].includes(comp.paneName)) {
this.collapsePane(comp.paneName);
}

Comment on lines +940 to +942
if (savedState.hideLeft) this.containerEl.classList.add('hide-left');
if (savedState.hideRight) this.containerEl.classList.add('hide-right');
if (savedState.hideBottom && this.centerEl) this.centerEl.classList.add('hide-bottom');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Manually adding the hide-* classes on load bypasses the custom element state updates and collapse button icon/title synchronization. This causes the collapse buttons to display incorrect icons (e.g., showing 'Collapse' instead of 'Expand') when starting with a collapsed pane. Reusing collapsePane ensures the UI is fully synchronized on initial load.

Suggested change
if (savedState.hideLeft) this.containerEl.classList.add('hide-left');
if (savedState.hideRight) this.containerEl.classList.add('hide-right');
if (savedState.hideBottom && this.centerEl) this.centerEl.classList.add('hide-bottom');
if (savedState.hideLeft) this.collapsePane('left');
if (savedState.hideRight) this.collapsePane('right');
if (savedState.hideBottom) this.collapsePane('bottom');

Comment on lines +248 to +250
this.addEventListener('pointermove', this._onPointerMove);
this.addEventListener('pointerup', this._onPointerUp);
this.addEventListener('pointercancel', this._onPointerUp);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Binding the pointermove, pointerup, and pointercancel listeners to this (the splitter element) can cause the drag operation to break if pointer capture fails or is lost and the pointer leaves the narrow 4px splitter boundaries. Binding these listeners to window ensures a highly robust and smooth dragging experience. Remember to also update the corresponding removeEventListener calls in _onPointerUp to target window.

Suggested change
this.addEventListener('pointermove', this._onPointerMove);
this.addEventListener('pointerup', this._onPointerUp);
this.addEventListener('pointercancel', this._onPointerUp);
window.addEventListener('pointermove', this._onPointerMove);
window.addEventListener('pointerup', this._onPointerUp);
window.addEventListener('pointercancel', this._onPointerUp);

Comment on lines +700 to +702
splitterEl.addEventListener('pointermove', onMove);
splitterEl.addEventListener('pointerup', onUp);
splitterEl.addEventListener('pointercancel', onUp);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Binding the pointermove, pointerup, and pointercancel listeners to splitterEl can cause the drag operation to break immediately when the pointer leaves the narrow 4px splitter boundaries if pointer capture fails or is not supported. Binding these listeners to window ensures robust dragging. Remember to also update the corresponding removeEventListener calls in onUp to target window.

Suggested change
splitterEl.addEventListener('pointermove', onMove);
splitterEl.addEventListener('pointerup', onUp);
splitterEl.addEventListener('pointercancel', onUp);
window.addEventListener('pointermove', onMove);
window.addEventListener('pointerup', onUp);
window.addEventListener('pointercancel', onUp);

Comment thread src/web/src/grid-layout.js Dismissed
@QuantamHD

Copy link
Copy Markdown
Collaborator Author

@gadfort would you mind giving this a try and seeing if it feels more stable than baseline?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants