Skip to content

Commit 4e971ab

Browse files
authored
[Improvement] Use mouse pos as origin when pressing +/- (#525)
* Use mouse pos as origin when pressing +/- * Zoom around minimap origin * Refactor zoom logic so we dont snap zoom when pressing + or - * Refactor zoom multiplier logic * Woops, swap var to let * Add missing key combo for zooming out * Request animation frame before zooming to ensure pan has completed * Fix lint error
1 parent cb10d99 commit 4e971ab

File tree

3 files changed

+75
-7
lines changed

3 files changed

+75
-7
lines changed

src/app-state/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {Atom} from '../lib/atom'
22
import {ViewMode} from '../lib/view-mode'
33
import {getHashParams, HashParams} from '../lib/hash-params'
44
import {ProfileGroupAtom} from './profile-group'
5+
import {Vec2} from '../lib/math'
56

67
// True if recursion should be flattened when viewing flamegraphs
78
export const flattenRecursionAtom = new Atom<boolean>(false, 'flattenRecursion')
@@ -51,6 +52,9 @@ export const loadingAtom = new Atom<boolean>(isImmediatelyLoading, 'loading')
5152
// imported was invalid.
5253
export const errorAtom = new Atom<boolean>(false, 'error')
5354

55+
// Minimap mouse position so we can zoom around mouse origin relative to minimap
56+
export const minimapMousePositionAtom = new Atom<Vec2 | null>(null, 'minimapMousePosition')
57+
5458
export enum SortField {
5559
SYMBOL_NAME,
5660
SELF,

src/views/flamechart-minimap-view.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {CanvasContext} from '../gl/canvas-context'
99
import {cachedMeasureTextWidth} from '../lib/text-utils'
1010
import {Color} from '../lib/color'
1111
import {Theme} from './themes/theme'
12+
import {minimapMousePositionAtom} from '../app-state'
1213

1314
interface FlamechartMinimapViewProps {
1415
theme: Theme
@@ -394,12 +395,16 @@ export class FlamechartMinimapView extends Component<FlamechartMinimapViewProps,
394395
if (this.draggingMode == null) {
395396
document.body.style.cursor = 'default'
396397
}
398+
// Clear the minimap mouse position when leaving the minimap
399+
minimapMousePositionAtom.set(null)
397400
}
398401

399402
private onMouseMove = (ev: MouseEvent) => {
400403
const configSpaceMouse = this.configSpaceMouse(ev)
401404
if (!configSpaceMouse) return
402405
this.updateCursor(configSpaceMouse)
406+
// Update the global minimap mouse position for zoom origin
407+
minimapMousePositionAtom.set(configSpaceMouse)
403408
}
404409

405410
private onWindowMouseUp = (ev: MouseEvent) => {

src/views/flamechart-pan-zoom-view.tsx

Lines changed: 66 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {ProfileSearchResults} from '../lib/profile-search'
1717
import {BatchCanvasTextRenderer, BatchCanvasRectRenderer} from '../lib/canvas-2d-batch-renderers'
1818
import {Color} from '../lib/color'
1919
import {Theme} from './themes/theme'
20+
import {minimapMousePositionAtom} from '../app-state'
2021

2122
interface FlamechartFrameLabel {
2223
configSpaceBounds: Rect
@@ -558,6 +559,7 @@ export class FlamechartPanZoomView extends Component<FlamechartPanZoomViewProps,
558559

559560
private lastDragPos: Vec2 | null = null
560561
private mouseDownPos: Vec2 | null = null
562+
private currentMousePos: Vec2 | null = null
561563
private onMouseDown = (ev: MouseEvent) => {
562564
this.mouseDownPos = this.lastDragPos = new Vec2(ev.offsetX, ev.offsetY)
563565
this.updateCursor()
@@ -623,6 +625,7 @@ export class FlamechartPanZoomView extends Component<FlamechartPanZoomViewProps,
623625
}
624626

625627
private onMouseMove = (ev: MouseEvent) => {
628+
this.currentMousePos = new Vec2(ev.offsetX, ev.offsetY)
626629
this.updateCursor()
627630
if (this.lastDragPos) {
628631
ev.preventDefault()
@@ -686,6 +689,7 @@ export class FlamechartPanZoomView extends Component<FlamechartPanZoomViewProps,
686689
}
687690

688691
private onMouseLeave = (ev: MouseEvent) => {
692+
this.currentMousePos = null
689693
this.hoveredLabel = null
690694
this.props.onNodeHover(null)
691695
this.renderCanvas()
@@ -728,12 +732,67 @@ export class FlamechartPanZoomView extends Component<FlamechartPanZoomViewProps,
728732
if (!this.container) return
729733
const {width, height} = this.container.getBoundingClientRect()
730734

731-
if (ev.key === '=' || ev.key === '+') {
732-
this.zoom(new Vec2(width / 2, height / 2), 0.5)
733-
ev.preventDefault()
734-
} else if (ev.key === '-' || ev.key === '_') {
735-
this.zoom(new Vec2(width / 2, height / 2), 2)
736-
ev.preventDefault()
735+
// Check if we have a minimap mouse position (user is hovering over minimap)
736+
// if we do, then pan the mouse to the minimap mouse position and then
737+
// perform zoom from the center of the main view
738+
//
739+
// If we aren't hovering over the minimap, then use the main view and
740+
// zoom around the mouse position, falling back to the center of
741+
// the main view if it's not available.
742+
const minimapMousePos = minimapMousePositionAtom.get()
743+
let zoomCenter: Vec2
744+
let shouldZoom = true
745+
746+
if (minimapMousePos) {
747+
const currentViewport = this.props.configSpaceViewportRect
748+
749+
// Check if the minimap mouse position is within the current viewport bounds
750+
const isWithinViewport =
751+
minimapMousePos.x >= currentViewport.left() &&
752+
minimapMousePos.x <= currentViewport.right() &&
753+
minimapMousePos.y >= currentViewport.top() &&
754+
minimapMousePos.y <= currentViewport.bottom()
755+
756+
// Pan to the minimap mouse position
757+
const newOrigin = new Vec2(
758+
minimapMousePos.x - currentViewport.width() / 2,
759+
minimapMousePos.y - currentViewport.height() / 2,
760+
)
761+
this.props.setConfigSpaceViewportRect(currentViewport.withOrigin(newOrigin))
762+
763+
// If the position was outside the viewport, just pan without zooming
764+
// Next +/- press will do the zoom
765+
if (!isWithinViewport) {
766+
shouldZoom = false
767+
}
768+
769+
zoomCenter = new Vec2(width / 2, height / 2)
770+
} else {
771+
zoomCenter = this.currentMousePos || new Vec2(width / 2, height / 2)
772+
}
773+
774+
// By default the zoom multiplier is 1 (no transformation), if
775+
// we are zooming in, scale the transform down by 0.5, else
776+
// scale up by 2 (this moves us 1 full grid line chunk at a time)
777+
let zoomMultiplier = 1
778+
779+
switch (ev.key) {
780+
case '=':
781+
case '+':
782+
zoomMultiplier = 0.5
783+
break
784+
case '-':
785+
case '_':
786+
zoomMultiplier = 2
787+
break
788+
}
789+
790+
if (shouldZoom) {
791+
// Slight delay to ensure we pan before zooming
792+
requestAnimationFrame(() => {
793+
this.zoom(zoomCenter, zoomMultiplier)
794+
ev.preventDefault()
795+
})
737796
}
738797

739798
if (ev.ctrlKey || ev.shiftKey || ev.metaKey) return
@@ -744,7 +803,7 @@ export class FlamechartPanZoomView extends Component<FlamechartPanZoomViewProps,
744803
//
745804
// See: https://github.com/jlfwong/speedscope/pull/184
746805
if (ev.key === '0') {
747-
this.zoom(new Vec2(width / 2, height / 2), 1e9)
806+
this.zoom(zoomCenter, 1e9)
748807
} else if (ev.key === 'ArrowRight' || ev.code === 'KeyD') {
749808
this.pan(new Vec2(100, 0))
750809
} else if (ev.key === 'ArrowLeft' || ev.code === 'KeyA') {

0 commit comments

Comments
 (0)