diff --git a/src/renderer/src/components/graph/GraphCanvas.tsx b/src/renderer/src/components/graph/GraphCanvas.tsx index fd014b9..ff3e0a6 100644 --- a/src/renderer/src/components/graph/GraphCanvas.tsx +++ b/src/renderer/src/components/graph/GraphCanvas.tsx @@ -273,7 +273,9 @@ export function GraphCanvas({ const clampView = useCallback(() => { const view = viewRef.current const { width, height } = sizeRef.current - const cs = contentSize(sceneRef.current.layout, sceneRef.current.wip?.column ?? null) + const cs = contentSize(sceneRef.current.layout, sceneRef.current.wip?.column ?? null, (row) => + labelWidthFor(row.name) + ) const cw = cs.width * view.scale const ch = cs.height * view.scale const clamp = (v: number, lo: number, hi: number) => Math.min(Math.max(v, lo), hi) @@ -358,7 +360,9 @@ export function GraphCanvas({ zoomAnim.stop() panInertia.cancel() const { width, height } = sizeRef.current - const cs = contentSize(sceneRef.current.layout, sceneRef.current.wip?.column ?? null) + const cs = contentSize(sceneRef.current.layout, sceneRef.current.wip?.column ?? null, (row) => + labelWidthFor(row.name) + ) const view = viewRef.current view.scale = Math.min( MAX_SCALE, diff --git a/src/renderer/src/components/graph/geometry.test.ts b/src/renderer/src/components/graph/geometry.test.ts index 905e669..0832201 100644 --- a/src/renderer/src/components/graph/geometry.test.ts +++ b/src/renderer/src/components/graph/geometry.test.ts @@ -305,4 +305,20 @@ describe('graph geometry', () => { 44 ) }) + + test('contentSize grows to cover a label pill overhanging its last column', () => { + const layout = sampleLayout() + const feature = layout.rows.find((r) => r.name === 'feature') + if (!feature) throw new Error('missing feature row') + // A name far wider than the row span: the pill's right edge (rest position + // plus the pill's own padding) plus the margin sets the content width. + const textWidth = 400 + const { width } = contentSize(layout, null, () => textWidth) + expect(width).toBe(nodeX(feature.startColumn) - NODE_R + textWidth + 16 + 28) + }) + + test('contentSize ignores labels that fit inside their row span', () => { + const layout = sampleLayout() + expect(contentSize(layout, null, () => 10)).toEqual(contentSize(layout, null)) + }) }) diff --git a/src/renderer/src/components/graph/geometry.ts b/src/renderer/src/components/graph/geometry.ts index f1135e5..961364d 100644 --- a/src/renderer/src/components/graph/geometry.ts +++ b/src/renderer/src/components/graph/geometry.ts @@ -134,16 +134,27 @@ export const toWorldY = (view: View, screenY: number): number => (screenY - view /** World-space size of the whole diagram. `wipColumn` is the WIP node's * column when it shows, or null. Both it and an empty branch's reserved slot * can sit past the last commit column, so width follows the rightmost of - * commits, row spans and the WIP node. */ + * commits, row spans and the WIP node — and, when `labelWidth` is given, of + * label pills too: a long branch name on a short row overhangs its last + * column, and pan/fit clamp to this size, so an uncounted overhang would be + * forever cut at the viewport edge. */ export function contentSize( layout: GraphLayout, - wipColumn: number | null + wipColumn: number | null, + labelWidth?: (row: GraphRow) => number ): { width: number; height: number } { let last = layout.columnCount - 1 for (const row of layout.rows) last = Math.max(last, row.endColumn) if (wipColumn !== null) last = Math.max(last, wipColumn) + let width = MARGIN_X * 2 + Math.max(1, last + 1) * COL_W + if (labelWidth) { + for (const row of layout.rows) { + const rect = labelRect(row, labelWidth(row)) + width = Math.max(width, rect.x + rect.w + MARGIN_X) + } + } return { - width: MARGIN_X * 2 + Math.max(1, last + 1) * COL_W, + width, height: MARGIN_Y + Math.max(1, layout.rowCount) * ROW_H + ROW_H / 2 } }