Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve CFG viewer navigation and behavior #5134

Merged
merged 3 commits into from
Jun 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion static/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -526,7 +526,6 @@ export function getCfgViewWith(id: number, editorid: number, treeid: number): Co
componentName: CFG_VIEW_COMPONENT_NAME,
componentState: {
selectedFunction: null,
zoom: 1,
id,
editorid,
treeid,
Expand Down
1 change: 0 additions & 1 deletion static/panes/cfg-view.interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@

export interface CfgState {
selectedFunction: string | null;
zoom: number;
}

/*
Expand Down
111 changes: 89 additions & 22 deletions static/panes/cfg-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,8 @@ export class Cfg extends Pane<CfgState> {
dragStartPosition: Coordinate = {x: 0, y: 0};
graphDimensions = {width: 0, height: 0};
functionSelector: TomSelect;
resetViewButton: JQuery;
zoomOutButton: JQuery;
results: CFGResult;
state: CfgState & PaneState;
layout: GraphLayoutCore;
Expand All @@ -130,6 +132,9 @@ export class Cfg extends Pane<CfgState> {
readonly extraTransforms: string;
fictitiousGraphContainer: HTMLDivElement;
fictitiousBlockContainer: HTMLDivElement;
zoom = 1;
// Ugly but I don't see another way
firstRender = true;

constructor(hub: Hub, container: Container, state: CfgState & PaneState) {
if ((state as any).selectedFn) {
Expand All @@ -139,7 +144,6 @@ export class Cfg extends Pane<CfgState> {
editorid: state.editorid,
treeid: state.treeid,
selectedFunction: (state as any).selectedFn,
zoom: 1,
};
}
super(hub, container, state);
Expand All @@ -149,7 +153,7 @@ export class Cfg extends Pane<CfgState> {
this.state = state;
// This is a workaround for a chrome render bug that's existed since at least 2013
// https://github.com/compiler-explorer/compiler-explorer/issues/4421
this.extraTransforms = !navigator.userAgent.includes('AppleWebKit') ? '' : ' translateZ(0)';
this.extraTransforms = navigator.userAgent.includes('AppleWebKit') ? ' translateZ(0)' : '';
}

override getInitialHTML() {
Expand All @@ -173,6 +177,14 @@ export class Cfg extends Pane<CfgState> {
sortField: 'title',
onChange: e => this.selectFunction(e as unknown as string),
});
this.resetViewButton = this.domRoot.find('.reset-view');
this.resetViewButton.on('click', () => {
this.resetView(true);
});
this.zoomOutButton = this.domRoot.find('.zoom-out');
this.zoomOutButton.on('click', () => {
this.birdsEyeView();
});
}

override registerOpeningAnalyticsEvent(): void {
Expand Down Expand Up @@ -235,30 +247,28 @@ export class Cfg extends Pane<CfgState> {
});
this.graphContainer.addEventListener('mousemove', e => {
if (this.dragging) {
this.currentPosition = {
this.setPan({
x: e.clientX - this.dragStart.x + this.dragStartPosition.x,
y: e.clientY - this.dragStart.y + this.dragStartPosition.y,
};
this.graphElement.style.left = this.currentPosition.x + 'px';
this.graphElement.style.top = this.currentPosition.y + 'px';
});
}
});
this.graphContainer.addEventListener('wheel', e => {
const delta = DZOOM * -Math.sign(e.deltaY) * Math.max(1, this.state.zoom - 1);
const prevZoom = this.state.zoom;
this.state.zoom += delta;
if (this.state.zoom >= MINZOOM) {
this.zoom(this.state.zoom);
const delta = DZOOM * -Math.sign(e.deltaY) * Math.max(1, this.zoom - 1);
const prevZoom = this.zoom;
const zoom = this.zoom + delta;
if (zoom >= MINZOOM) {
this.setZoom(zoom);
const mouseX = e.clientX - this.graphElement.getBoundingClientRect().x;
const mouseY = e.clientY - this.graphElement.getBoundingClientRect().y;
// Amount that the zoom will offset is mouseX / width before zoom * delta * unzoomed width
// And same for y. The width / height terms cancel.
this.currentPosition.x -= (mouseX / prevZoom) * delta;
this.currentPosition.y -= (mouseY / prevZoom) * delta;
this.graphElement.style.left = this.currentPosition.x + 'px';
this.graphElement.style.top = this.currentPosition.y + 'px';
this.setPan({
x: this.currentPosition.x - (mouseX / prevZoom) * delta,
y: this.currentPosition.y - (mouseY / prevZoom) * delta,
});
} else {
this.state.zoom = MINZOOM;
this.setZoom(MINZOOM);
}
e.preventDefault();
});
Expand Down Expand Up @@ -324,7 +334,6 @@ export class Cfg extends Pane<CfgState> {
selectedFunction = keys[0];
}
this.functionSelector.setValue(selectedFunction, true);
this.state.selectedFunction = selectedFunction;
} else {
// this.state.selectedFunction won't change, next time the compilation results aren't errors or empty
// the selected function will still be the same
Expand Down Expand Up @@ -484,8 +493,8 @@ export class Cfg extends Pane<CfgState> {
}
}

// display the cfg for the specified function if it exists
// this function does not change or use this.state.selectedFunction
// Display the cfg for the specified function if it exists
// This function sets this.state.selectedFunction if the input is non-null and valid
async selectFunction(name: string | null) {
$('.fold').popover('dispose');
this.blockContainer.innerHTML = '';
Expand All @@ -506,14 +515,60 @@ export class Cfg extends Pane<CfgState> {
this.estimatedPNGSize.innerHTML = `(~${size_to_human(
this.layout.getWidth() * this.layout.getHeight() * 4 * EST_COMPRESSION_RATIO,
)})`;
if (this.state.selectedFunction !== name || this.firstRender) {
this.resetView();
this.firstRender = false;
}
this.state.selectedFunction = name;
this.updateState();
}

resetView(resetZoom?: boolean) {
// If we have selected a new function, or this is the first load, reset zoom and pan to the function entry
if (this.layout.blocks.length > 0) {
if (resetZoom) {
this.setZoom(1);
}
const entry_pos = this.layout.blocks[0].coordinates;
const container_size = this.graphContainer.getBoundingClientRect();
const entry_size = this.bbMap[this.layout.blocks[0].data.id].getBoundingClientRect();
this.setPan({
// entry_size will already have the zoom factored in
x: -(entry_pos.x * this.zoom) + container_size.width / 2 - entry_size.width / 2,
y: entry_pos.y * this.zoom,
});
}
}

zoom(zoom: number) {
birdsEyeView() {
if (this.layout.blocks.length > 0) {
const fullW = this.layout.getWidth();
const fullH = this.layout.getHeight();
const container_size = this.graphContainer.getBoundingClientRect();
const zoom = Math.min(container_size.width / fullW, container_size.height / fullH);
this.setZoom(zoom);
this.setPan({
x: container_size.width / 2 - (fullW * zoom) / 2,
y: container_size.height / 2 - (fullH * zoom) / 2,
});
}
}

setZoom(zoom: number, superficial?: boolean) {
this.graphElement.style.transform = `scale(${zoom})${this.extraTransforms}`;
if (!superficial) {
this.zoom = zoom;
}
}

setPan(p: Coordinate) {
this.currentPosition = p;
this.graphElement.style.left = this.currentPosition.x + 'px';
this.graphElement.style.top = this.currentPosition.y + 'px';
}

createSVG() {
this.zoom(1);
this.setZoom(1, true);
let doc = '';
doc += '<?xml version="1.0"?>';
doc += '<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">';
Expand Down Expand Up @@ -567,7 +622,7 @@ export class Cfg extends Pane<CfgState> {
}
}
doc += '</svg>';
this.zoom(this.state.zoom);
this.setZoom(this.zoom, true);
return doc;
}

Expand Down Expand Up @@ -614,6 +669,18 @@ export class Cfg extends Pane<CfgState> {
});
}

override getCurrentState(): CfgState & PaneState {
const state = {
id: this.compilerInfo.compilerId,
compilerName: this.compilerInfo.compilerName,
editorid: this.compilerInfo.editorId,
treeid: this.compilerInfo.treeId,
selectedFunction: this.state.selectedFunction,
};
this.paneRenaming.addState(state);
return state;
}

override close(): void {
this.eventHub.unsubscribe();
this.eventHub.emit('cfgViewClosed', this.compilerInfo.compilerId);
Expand Down
6 changes: 6 additions & 0 deletions views/templates/panes/cfg.pug
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@
button.btn.btn-sm.btn-light.dropdown-item.export-svg(title="Export graph" aria-label="Export graph as an image")
span.fas.fa-vector-square
span.hideable Export SVG
.btn-group.btn-group-sm(role="group")
button.btn.btn-sm.btn-light.reset-view(title="Reset View")
span <i class="fa-solid fa-arrows-to-eye"></i> Reset View
.btn-group.btn-group-sm(role="group")
button.btn.btn-sm.btn-light.zoom-out(title="Reset View")
span <i class="fa-solid fa-expand"></i> Zoom Out
.graph-container
span.cfg-info
.graph
Expand Down
Loading