Skip to content

Commit e1d1df1

Browse files
authored
Right Panel fixes (#13557)
Closes #13539 Part of #13222 1. The buttons in Graph are aligned to those in Description/Documentation editor. <img width="2470" height="1287" alt="image" src="https://github.com/user-attachments/assets/969d27e5-8ac1-4e62-b913-456a4e8182ac" /> 2. The graph editor "anchor" point is now left-top corner instead of center: this makes nodes not move when opening/resizing right and bottom panel. https://github.com/user-attachments/assets/c117b972-0593-472a-9f92-f6cbf2f28bf1
1 parent 4287f51 commit e1d1df1

File tree

10 files changed

+68
-59
lines changed

10 files changed

+68
-59
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
- ["Grouped Components" are renamed to "User Defined Components"][13389]
3030
- [Allow displaying cloud images using enso:// url in documentation][13419]
3131
- [File Browser Widget is used when editing file paths to datalinks][13439]
32+
- [Graph is not moved when showing/resizing side panels.][13557]
3233
- [Add "Invite" button to the top bar when using a team or higher plan][13522]
3334
- ["Welcome Project" is automatically opened for new users][13479]
3435
- [Project and Setting tab may be now closed with shortcut][13498]. On
@@ -57,6 +58,7 @@
5758
[13389]: https://github.com/enso-org/enso/pull/13389
5859
[13419]: https://github.com/enso-org/enso/pull/13419
5960
[13439]: https://github.com/enso-org/enso/pull/13439
61+
[13557]: https://github.com/enso-org/enso/pull/13557
6062
[13522]: https://github.com/enso-org/enso/pull/13522
6163
[13479]: https://github.com/enso-org/enso/pull/13479
6264
[13498]: https://github.com/enso-org/enso/pull/13498

app/gui/integration-test/project-view/collapsingAndEntering.spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,8 @@ async function expectInsideFunc1(page: Page) {
310310

311311
async function expectInsideFunc2(page: Page) {
312312
await actions.expectNodePositionsInitialized(page, -88)
313+
// The mouse is often in input's output port area, making our checks fooled by the edge ghost.
314+
await page.mouse.move(0, 0)
313315
await expect(locate.graphNode(page)).toHaveCount(3)
314316
await expect(locate.inputNode(page)).toHaveCount(1)
315317
await expect(locate.graphNodeByBinding(page, 'r')).toExist()

app/gui/src/project-view/ProjectView.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
markRaw,
1515
onActivated,
1616
onDeactivated,
17+
onMounted,
1718
onScopeDispose,
1819
ref,
1920
toRaw,
@@ -70,6 +71,7 @@ provideVisibility(visible)
7071
openedProjects.registerProject(toRefs(props))
7172
onScopeDispose(() => openedProjects.unregisterProject(props.projectId))
7273
74+
onMounted(() => (visible.value = true))
7375
onActivated(() => (visible.value = true))
7476
onDeactivated(() => (visible.value = false))
7577
</script>

app/gui/src/project-view/components/SceneScroller.vue

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,15 @@ const scrollingState = ref<{
2525
readonly range: Vec2
2626
/** `pos` when scrolling started. */
2727
readonly scrollStartPos: Vec2
28-
/** Viewport center, in scene coordinates, when scrolling started. */
28+
/** Viewport left top corner, in scene coordinates, when scrolling started. */
2929
readonly scrollOrigin: Vec2
3030
}>()
3131
3232
function scroll(event: ScrollbarEvent) {
3333
switch (event.type) {
3434
case 'start': {
3535
const scrollStartPos = scrollInputs.value.pos
36-
const scrollOrigin = props.navigator.viewport.center()
36+
const scrollOrigin = props.navigator.viewport.pos
3737
scrollingState.value = { ...scrollInputs.value, scrollStartPos, scrollOrigin }
3838
break
3939
}
@@ -56,7 +56,7 @@ function scroll(event: ScrollbarEvent) {
5656
const proportionalPos = event.position / scrollInputs.value.range.getAxis(event.axis)
5757
const scaledPos = proportionalPos * props.scrollableArea.size.getAxis(event.axis)
5858
const pos = scaledPos + props.scrollableArea.pos.getAxis(event.axis)
59-
props.navigator.scrollTo(props.navigator.viewport.center().setAxis(event.axis, pos))
59+
props.navigator.scrollTo(props.navigator.viewport.pos.setAxis(event.axis, pos))
6060
break
6161
}
6262
}

app/gui/src/project-view/components/TopBar.vue

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,12 @@ const selection = injectGraphSelection()
4040
position: absolute;
4141
display: flex;
4242
gap: 8px;
43-
top: 8px;
43+
top: 1.25rem;
4444
left: 0;
4545
right: 0;
46-
margin-left: 11px;
47-
margin-right: 11px;
46+
margin-top: -3px;
47+
margin-left: 13px;
48+
margin-right: 13px;
4849
pointer-events: none;
4950
align-items: flex-start;
5051
> * {

app/gui/src/project-view/composables/__tests__/navigator.test.ts

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,46 +17,48 @@ describe('useNavigator', () => {
1717
})
1818
}
1919

20-
test('initializes with centered non-zoomed viewport', async () => {
20+
test('initializes with non-zoomed viewport', async () => {
2121
const navigator = await makeTestNavigator()
22-
expect(navigator.viewport).toStrictEqual(Rect.FromBounds(-400, -200, 400, 200))
22+
expect(navigator.viewport).toStrictEqual(Rect.FromBounds(0, 0, 800, 400))
2323
})
2424

2525
test('clientToScenePos without scaling', async () => {
2626
const navigator = await makeTestNavigator()
27-
expect(navigator.clientToScenePos(Vec2.Zero)).toStrictEqual(new Vec2(-550, -350))
28-
expect(navigator.clientToScenePos(new Vec2(150, 150))).toStrictEqual(new Vec2(-400, -200))
29-
expect(navigator.clientToScenePos(new Vec2(550, 350))).toStrictEqual(new Vec2(0, 0))
27+
expect(navigator.clientToScenePos(Vec2.Zero)).toStrictEqual(new Vec2(-150, -150))
28+
expect(navigator.clientToScenePos(new Vec2(150, 150))).toStrictEqual(new Vec2(0, 0))
29+
expect(navigator.clientToScenePos(new Vec2(550, 350))).toStrictEqual(new Vec2(400, 200))
30+
expect(navigator.clientToScenePos(new Vec2(950, 550))).toStrictEqual(new Vec2(800, 400))
3031
})
3132

3233
test('clientToScenePos with scaling', async () => {
3334
const navigator = await makeTestNavigator()
34-
navigator.setCenterAndScale(Vec2.Zero, 2)
35-
expect(navigator.clientToScenePos(Vec2.Zero)).toStrictEqual(new Vec2(-275, -175))
36-
expect(navigator.clientToScenePos(new Vec2(150, 150))).toStrictEqual(new Vec2(-200, -100))
37-
expect(navigator.clientToScenePos(new Vec2(550, 350))).toStrictEqual(new Vec2(0, 0))
35+
navigator.setPosAndScale(Vec2.Zero, 2)
36+
expect(navigator.clientToScenePos(Vec2.Zero)).toStrictEqual(new Vec2(-75, -75))
37+
expect(navigator.clientToScenePos(new Vec2(150, 150))).toStrictEqual(new Vec2(0, 0))
38+
expect(navigator.clientToScenePos(new Vec2(550, 350))).toStrictEqual(new Vec2(200, 100))
39+
expect(navigator.clientToScenePos(new Vec2(950, 550))).toStrictEqual(new Vec2(400, 200))
3840
})
3941

4042
test('clientToSceneRect without scaling', async () => {
4143
const navigator = await makeTestNavigator()
42-
expect(navigator.clientToSceneRect(Rect.Zero)).toStrictEqual(Rect.XYWH(-550, -350, 0, 0))
44+
expect(navigator.clientToSceneRect(Rect.Zero)).toStrictEqual(Rect.XYWH(-150, -150, 0, 0))
4345
expect(navigator.clientToSceneRect(Rect.XYWH(150, 150, 800, 400))).toStrictEqual(
4446
navigator.viewport,
4547
)
4648
expect(navigator.clientToSceneRect(Rect.XYWH(100, 150, 200, 900))).toStrictEqual(
47-
Rect.XYWH(-450, -200, 200, 900),
49+
Rect.XYWH(-50, 0, 200, 900),
4850
)
4951
})
5052

5153
test('clientToSceneRect with scaling', async () => {
5254
const navigator = await makeTestNavigator()
53-
navigator.setCenterAndScale(Vec2.Zero, 2)
54-
expect(navigator.clientToSceneRect(Rect.Zero)).toStrictEqual(Rect.XYWH(-275, -175, 0, 0))
55+
navigator.setPosAndScale(Vec2.Zero, 2)
56+
expect(navigator.clientToSceneRect(Rect.Zero)).toStrictEqual(Rect.XYWH(-75, -75, 0, 0))
5557
expect(navigator.clientToSceneRect(Rect.XYWH(150, 150, 800, 400))).toStrictEqual(
5658
navigator.viewport,
5759
)
5860
expect(navigator.clientToSceneRect(Rect.XYWH(100, 150, 200, 900))).toStrictEqual(
59-
Rect.XYWH(-225, -100, 100, 450),
61+
Rect.XYWH(-25, 0, 100, 450),
6062
)
6163
})
6264
})

app/gui/src/project-view/composables/__tests__/selection.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ function selectionWithMockData() {
2020
const mockDom = document.createElement('div')
2121
mockDom.style.width = '500px'
2222
mockDom.style.height = '300px'
23-
vi.spyOn(mockDom, 'getBoundingClientRect').mockReturnValue(new DOMRect(-250, -150, 500, 300))
23+
vi.spyOn(mockDom, 'getBoundingClientRect').mockReturnValue(new DOMRect(0, 0, 500, 300))
2424

2525
const navigator = useNavigator(ref(mockDom), useKeyboard())
2626

app/gui/src/project-view/composables/navigator.ts

Lines changed: 31 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,8 @@ export function useNavigator(
5454
) {
5555
const predicate = options.predicate ?? ((_) => true)
5656
const size = useResizeObserver(viewportNode)
57-
const targetCenter = shallowRef<Vec2>(Vec2.Zero)
58-
const center = useApproachVec(targetCenter, 100, 0.02)
57+
const targetLeftTop = shallowRef<Vec2>(Vec2.Zero)
58+
const leftTop = useApproachVec(targetLeftTop, 100, 0.2)
5959

6060
const viewportRect = shallowRef<Rect>(Rect.Zero)
6161
const viewportElem = computed(() => unrefElement(viewportNode))
@@ -83,15 +83,15 @@ export function useNavigator(
8383

8484
function handleDragPanning(state: DragState) {
8585
if (Vec2.FromTuple(state.movement).length() > LONGPRESS_MAX_SLIDE) cancelLongpress()
86-
scrollTo(center.value.addScaled(Vec2.FromTuple(state.delta), -1 / scale.value))
86+
scrollTo(leftTop.value.addScaled(Vec2.FromTuple(state.delta), -1 / scale.value))
8787
state.event.stopImmediatePropagation()
8888
}
8989

9090
function handleDragZooming(state: DragState) {
9191
if (state.delta[1] != 0) preventContextMenu = true
9292
const prevScale = scale.value
9393
updateScale((oldValue) => oldValue * Math.exp(-state.delta[1] / 100))
94-
scrollTo(center.value.scaleAround(prevScale / scale.value, gesturePivot))
94+
scrollTo(leftTop.value.scaleAround(prevScale / scale.value, gesturePivot))
9595
}
9696

9797
function handleDragHolding(state: DragState) {
@@ -186,15 +186,15 @@ export function useNavigator(
186186
const prevScale = scale.value
187187
updateScale((_) => pinchScaleRatio * state.da[0])
188188
scrollTo(
189-
center.value
189+
leftTop.value
190190
.scaleAround(prevScale / scale.value, gesturePivot)
191191
.addScaled(originDelta, -1 / scale.value),
192192
)
193193
},
194194
onWheel(state) {
195195
if (state.ctrlKey) return
196196
const delta = Vec2.FromTuple(state.delta)
197-
scrollTo(center.value.addScaled(delta, 1 / scale.value))
197+
scrollTo(leftTop.value.addScaled(delta, 1 / scale.value))
198198
},
199199
},
200200
{
@@ -221,7 +221,7 @@ export function useNavigator(
221221
const scale = useApproach(targetScale)
222222

223223
const panArrows = useArrows(
224-
(pos) => scrollTo(center.value.addScaled(pos.delta, 1 / scale.value)),
224+
(pos) => scrollTo(leftTop.value.addScaled(pos.delta, 1 / scale.value)),
225225
{ predicate, velocity: 1000 },
226226
)
227227

@@ -270,10 +270,16 @@ export function useNavigator(
270270
minScale,
271271
maxScale,
272272
)
273-
targetCenter.value = rect.center().finiteOrZero()
273+
// Rect position should be centered inside viewport (they may have different aspect ratio).
274+
const w = viewportElem.value.clientWidth / targetScale.value
275+
const h = viewportElem.value.clientHeight / targetScale.value
276+
targetLeftTop.value = rect
277+
.center()
278+
.finiteOrZero()
279+
.sub(new Vec2(w / 2, h / 2))
274280
if (skipAnimation) {
275281
scale.skip()
276-
center.skip()
282+
leftTop.skip()
277283
}
278284
}
279285

@@ -320,32 +326,31 @@ export function useNavigator(
320326
function panToImpl(points: Partial<Vec2>[]) {
321327
let target = viewport.value
322328
for (const point of points.reverse()) target = target.offsetToInclude(point) ?? target
323-
targetCenter.value = target.center().finiteOrZero()
329+
targetLeftTop.value = target.pos.finiteOrZero()
324330
}
325331

326332
/** Pan immediately to center the viewport at the given point, in scene coordinates. */
327-
function scrollTo(newCenter: Vec2) {
333+
function scrollTo(newLeftTop: Vec2) {
328334
resetTargetFollowing()
329-
targetCenter.value = newCenter.finiteOrZero()
330-
center.skip()
335+
targetLeftTop.value = newLeftTop.finiteOrZero()
336+
leftTop.skip()
331337
}
332338

333-
/** Set viewport center point and scale value immediately, skipping animations. */
334-
function setCenterAndScale(newCenter: Vec2, newScale: number) {
339+
/** Set viewport left-top point and scale value immediately, skipping animations. */
340+
function setPosAndScale(newLeftTop: Vec2, newScale: number) {
335341
resetTargetFollowing()
336-
targetCenter.value = newCenter.finiteOrZero()
342+
targetLeftTop.value = newLeftTop.finiteOrZero()
337343
targetScale.value = newScale
338344
scale.skip()
339-
center.skip()
345+
leftTop.skip()
340346
}
341347

342348
const viewport = computed(() => {
343349
const nodeSize = size.value
344-
const { x, y } = center.value
345350
const s = scale.value
346351
const w = nodeSize.x / s
347352
const h = nodeSize.y / s
348-
return new Rect(new Vec2(x - w / 2, y - h / 2), new Vec2(w, h))
353+
return new Rect(leftTop.value, new Vec2(w, h))
349354
})
350355

351356
const viewBox = computed(() => {
@@ -354,16 +359,11 @@ export function useNavigator(
354359
})
355360

356361
const translate = computed<Vec2>(() => {
357-
const nodeSize = size.value
358-
const { x, y } = center.value
359-
const s = scale.value
360-
const w = nodeSize.x / s
361-
const h = nodeSize.y / s
362-
return new Vec2(-x + w / 2, -y + h / 2)
362+
return leftTop.value.scale(-1)
363363
})
364364

365365
const transformChanging = computed(
366-
() => scale.active || center.active || dragActive.value || pinchActive.value,
366+
() => scale.active || leftTop.active || dragActive.value || pinchActive.value,
367367
)
368368
const transform = computed(
369369
() => `scale(${scale.value}) translate(${translate.value.x}px, ${translate.value.y}px)`,
@@ -428,8 +428,8 @@ export function useNavigator(
428428
const scenePos0 = clientToScenePos(clientPos)
429429
const result = f()
430430
const scenePos1 = clientToScenePos(clientPos)
431-
targetCenter.value = center.value.add(scenePos0.sub(scenePos1)).finiteOrZero()
432-
center.skip()
431+
targetLeftTop.value = leftTop.value.add(scenePos0.sub(scenePos1)).finiteOrZero()
432+
leftTop.skip()
433433
return result
434434
}
435435

@@ -449,7 +449,7 @@ export function useNavigator(
449449
},
450450
(e) => {
451451
const delta = new Vec2(e.deltaX, e.deltaY)
452-
scrollTo(center.value.addScaled(delta, 1 / scale.value))
452+
scrollTo(leftTop.value.addScaled(delta, 1 / scale.value))
453453
},
454454
)
455455

@@ -463,7 +463,7 @@ export function useNavigator(
463463
return proxyRefs({
464464
keyboardEvents: panArrows.events,
465465
translate: readonly(translate),
466-
targetCenter: readonly(targetCenter),
466+
targetLeftTop: readonly(targetLeftTop),
467467
targetScale: readonly(targetScale),
468468
scale: readonly(toRef(scale, 'value')),
469469
viewBox: readonly(viewBox),
@@ -489,6 +489,6 @@ export function useNavigator(
489489
viewport,
490490
stepZoom,
491491
scrollTo,
492-
setCenterAndScale,
492+
setPosAndScale,
493493
})
494494
}

app/gui/src/project-view/stores/persisted.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,16 +57,16 @@ export const [providePersisted, usePersisted] = createContextStore(
5757
debounce: 200,
5858
captureState() {
5959
return {
60-
x: graphNavigator.targetCenter.x,
61-
y: graphNavigator.targetCenter.y,
60+
x: graphNavigator.targetLeftTop.x,
61+
y: graphNavigator.targetLeftTop.y,
6262
s: graphNavigator.targetScale,
6363
} satisfies GraphStoredState
6464
},
6565
async restoreState(restored, abort) {
6666
if (restored) {
6767
const pos = new Vec2(restored.x ?? 0, restored.y ?? 0)
6868
const scale = restored.s ?? 1
69-
graphNavigator.setCenterAndScale(pos, scale)
69+
graphNavigator.setPosAndScale(pos, scale)
7070
} else {
7171
await until(visibleAreasReady).toBe(true)
7272
await until(visible).toBe(true)

app/gui/src/project-view/util/data/vec2.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,22 +60,22 @@ export class Vec2 {
6060
return Math.abs(this.x - other.x) < epsilon && Math.abs(this.y - other.y) < epsilon
6161
}
6262

63-
/** TODO: Add docs */
63+
/** Check if both coords are 0. */
6464
isZero(): boolean {
6565
return this.x === 0 && this.y === 0
6666
}
6767

68-
/** TODO: Add docs */
68+
/** Check if both coords have finite values. */
6969
isFinite(): boolean {
7070
return Number.isFinite(this.x) && Number.isFinite(this.y)
7171
}
7272

73-
/** TODO: Add docs */
73+
/** Change non-finite values in vector to 0. */
7474
finiteOrZero(): Vec2 {
7575
return new Vec2(Number.isFinite(this.x) ? this.x : 0, Number.isFinite(this.y) ? this.y : 0)
7676
}
7777

78-
/** TODO: Add docs */
78+
/** Multiply values by given value. */
7979
scale(scalar: number): Vec2 {
8080
return new Vec2(this.x * scalar, this.y * scalar)
8181
}

0 commit comments

Comments
 (0)