From c61e397658371a2c43bc633918e946ca8c15a779 Mon Sep 17 00:00:00 2001 From: Kaz Wesley Date: Tue, 12 Mar 2024 12:44:29 -0400 Subject: [PATCH] Scrollbars (#9310) Scrollbars --- app/gui2/mock/providers.ts | 1 + app/gui2/src/components/GraphEditor.vue | 22 +-- .../src/components/GraphEditor/GraphEdge.vue | 2 +- app/gui2/src/components/SceneScroller.vue | 65 +++++++ app/gui2/src/components/ScrollBar.vue | 163 ++++++++++++++++++ app/gui2/src/composables/navigator.ts | 12 +- app/gui2/src/util/data/__tests__/rect.test.ts | 2 +- app/gui2/src/util/data/rect.ts | 16 +- app/gui2/src/util/data/vec2.ts | 26 ++- 9 files changed, 284 insertions(+), 25 deletions(-) create mode 100644 app/gui2/src/components/SceneScroller.vue create mode 100644 app/gui2/src/components/ScrollBar.vue diff --git a/app/gui2/mock/providers.ts b/app/gui2/mock/providers.ts index 3614d7835ae0..e8667ce2d6ea 100644 --- a/app/gui2/mock/providers.ts +++ b/app/gui2/mock/providers.ts @@ -9,6 +9,7 @@ export const graphNavigator: GraphNavigator = { clientToSceneRect: () => Rect.Zero, panAndZoomTo: () => {}, panTo: () => {}, + scrollTo: () => {}, stepZoom: () => {}, transform: '', prescaledTransform: '', diff --git a/app/gui2/src/components/GraphEditor.vue b/app/gui2/src/components/GraphEditor.vue index 6c1dfbcc9084..aedfc5a7ef33 100644 --- a/app/gui2/src/components/GraphEditor.vue +++ b/app/gui2/src/components/GraphEditor.vue @@ -15,6 +15,7 @@ import { performCollapse, prepareCollapsedInfo } from '@/components/GraphEditor/ import { Uploader, uploadedExpression } from '@/components/GraphEditor/upload' import GraphMouse from '@/components/GraphMouse.vue' import PlusButton from '@/components/PlusButton.vue' +import SceneScroller from '@/components/SceneScroller.vue' import TopBar from '@/components/TopBar.vue' import { useDoubleClick } from '@/composables/doubleClick' import { keyboardBusy, keyboardBusyExceptIn, useEvent } from '@/composables/events' @@ -190,25 +191,14 @@ onMounted(() => viewportNode.value?.focus()) function zoomToSelected() { if (!viewportNode.value) return - let left = Infinity - let top = Infinity - let right = -Infinity - let bottom = -Infinity const nodesToCenter = nodeSelection.selected.size === 0 ? graphStore.db.nodeIdToNode.keys() : nodeSelection.selected + let bounds = Rect.Bounding() for (const id of nodesToCenter) { const rect = graphStore.vizRects.get(id) ?? graphStore.nodeRects.get(id) - if (!rect) continue - left = Math.min(left, rect.left) - right = Math.max(right, rect.right) - top = Math.min(top, rect.top) - bottom = Math.max(bottom, rect.bottom) + if (rect) bounds = Rect.Bounding(bounds, rect) } - graphNavigator.panAndZoomTo( - Rect.FromBounds(left, top, right, bottom), - 0.1, - Math.max(1, graphNavigator.scale), - ) + graphNavigator.panAndZoomTo(bounds, 0.1, Math.max(1, graphNavigator.scale)) } const graphBindingsHandler = graphBindings.handler({ @@ -651,6 +641,10 @@ function handleEdgeDrop(source: AstId, position: Vec2) { + diff --git a/app/gui2/src/components/GraphEditor/GraphEdge.vue b/app/gui2/src/components/GraphEditor/GraphEdge.vue index 30aea5d6f800..3859e5adf1e8 100644 --- a/app/gui2/src/components/GraphEditor/GraphEdge.vue +++ b/app/gui2/src/components/GraphEditor/GraphEdge.vue @@ -354,7 +354,7 @@ function lengthTo(path: SVGPathElement, pos: Vec2): number { let best: number | undefined let bestDist: number | undefined const tryPos = (len: number) => { - const dist = pos.distanceSquared(Vec2.FromDomPoint(path.getPointAtLength(len))) + const dist = pos.distanceSquared(Vec2.FromXY(path.getPointAtLength(len))) if (bestDist == null || dist < bestDist) { best = len bestDist = dist diff --git a/app/gui2/src/components/SceneScroller.vue b/app/gui2/src/components/SceneScroller.vue new file mode 100644 index 000000000000..962723215087 --- /dev/null +++ b/app/gui2/src/components/SceneScroller.vue @@ -0,0 +1,65 @@ + + + diff --git a/app/gui2/src/components/ScrollBar.vue b/app/gui2/src/components/ScrollBar.vue new file mode 100644 index 000000000000..3862925c6cc4 --- /dev/null +++ b/app/gui2/src/components/ScrollBar.vue @@ -0,0 +1,163 @@ + + + + + + diff --git a/app/gui2/src/composables/navigator.ts b/app/gui2/src/composables/navigator.ts index 1af695c70d72..b09e330d6630 100644 --- a/app/gui2/src/composables/navigator.ts +++ b/app/gui2/src/composables/navigator.ts @@ -13,10 +13,8 @@ const PAN_AND_ZOOM_DEFAULT_SCALE_RANGE: ScaleRange = [0.1, 1] const ZOOM_STEP_DEFAULT_SCALE_RANGE: ScaleRange = [0.1, 10] function elemRect(target: Element | undefined): Rect { - if (target != null && target instanceof Element) { - const domRect = target.getBoundingClientRect() - return new Rect(new Vec2(domRect.x, domRect.y), new Vec2(domRect.width, domRect.height)) - } + if (target != null && target instanceof Element) + return Rect.FromDomRect(target.getBoundingClientRect()) return Rect.Zero } @@ -118,6 +116,11 @@ export function useNavigator(viewportNode: Ref) { targetCenter.value = target.center() } + /** Pan immediately to center the viewport at the given point, in scene coordinates. */ + function scrollTo(newCenter: Vec2) { + center.value = newCenter + } + let zoomPivot = Vec2.Zero const zoomPointer = usePointer((pos, _event, ty) => { if (ty === 'start') { @@ -329,5 +332,6 @@ export function useNavigator(viewportNode: Ref) { panTo, viewport, stepZoom, + scrollTo, }) } diff --git a/app/gui2/src/util/data/__tests__/rect.test.ts b/app/gui2/src/util/data/__tests__/rect.test.ts index e5baf46d6473..8877653634cf 100644 --- a/app/gui2/src/util/data/__tests__/rect.test.ts +++ b/app/gui2/src/util/data/__tests__/rect.test.ts @@ -11,7 +11,7 @@ test.prop({ x: fc.nat(), y: fc.nat(), })('offsetToInclude', ({ rectX, rectY, width, height, x, y }) => { - const rect = new Rect(new Vec2(rectX, rectY), new Vec2(width, height)) + const rect = Rect.XYWH(rectX, rectY, width, height) const point = new Vec2(x, y) const offsetRect = rect.offsetToInclude(point) expect( diff --git a/app/gui2/src/util/data/rect.ts b/app/gui2/src/util/data/rect.ts index 66e79b1fb8d5..7094ca5a07a6 100644 --- a/app/gui2/src/util/data/rect.ts +++ b/app/gui2/src/util/data/rect.ts @@ -24,7 +24,21 @@ export class Rect { } static FromDomRect(domRect: DOMRect): Rect { - return new Rect(new Vec2(domRect.x, domRect.y), new Vec2(domRect.width, domRect.height)) + return new Rect(Vec2.FromXY(domRect), Vec2.FromSize(domRect)) + } + + static Bounding(...rects: Rect[]): Rect { + let left = NaN + let top = NaN + let right = NaN + let bottom = NaN + for (const rect of rects) { + if (!(rect.left >= left)) left = rect.left + if (!(rect.top >= top)) top = rect.top + if (!(rect.right <= right)) right = rect.right + if (!(rect.bottom <= bottom)) bottom = rect.bottom + } + return this.FromBounds(left, top, right, bottom) } offsetBy(offset: Vec2): Rect { diff --git a/app/gui2/src/util/data/vec2.ts b/app/gui2/src/util/data/vec2.ts index d4b0353914d3..c46b143bd131 100644 --- a/app/gui2/src/util/data/vec2.ts +++ b/app/gui2/src/util/data/vec2.ts @@ -11,12 +11,26 @@ export class Vec2 { static Zero: Vec2 - static FromArr(arr: [number, number]): Vec2 { - return new Vec2(arr[0], arr[1]) + static FromXY(point: Readonly<{ x: number; y: number }>): Vec2 { + return new Vec2(point.x, point.y) } - static FromDomPoint(point: DOMPoint): Vec2 { - return new Vec2(point.x, point.y) + static FromSize(point: Readonly<{ width: number; height: number }>): Vec2 { + return new Vec2(point.width, point.height) + } + + static FromClientSize(point: Readonly<{ clientWidth: number; clientHeight: number }>): Vec2 { + return new Vec2(point.clientWidth, point.clientHeight) + } + + static ElementwiseProduct(...values: Vec2[]): Vec2 { + let x = 1 + let y = 1 + for (const value of values) { + x *= value.x + y *= value.y + } + return new Vec2(x, y) } equals(other: Vec2): boolean { @@ -41,6 +55,10 @@ export class Vec2 { return new Vec2(-this.x, -this.y) } + reciprocal(): Vec2 { + return new Vec2(1 / this.x, 1 / this.y) + } + add(other: Vec2): Vec2 { return new Vec2(this.x + other.x, this.y + other.y) }