summaryrefslogtreecommitdiff
path: root/internal/tui
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-05-26 22:48:54 +0300
committerPaul Buetow <paul@buetow.org>2026-05-26 22:48:54 +0300
commitff8ef868472dd3b359c03e4ff9163bf2d113e0bf (patch)
tree26690368120ad431956f9a1803b90829e78786e3 /internal/tui
parentfb5a9c1f5c99559cb013a6ff396eb56a7d1f7be6 (diff)
vo: fix flamegraph click mapping for expanded leaf rows
Diffstat (limited to 'internal/tui')
-rw-r--r--internal/tui/flamegraph/frame_animator.go35
-rw-r--r--internal/tui/flamegraph/frame_animator_test.go48
-rw-r--r--internal/tui/flamegraph/model.go4
3 files changed, 80 insertions, 7 deletions
diff --git a/internal/tui/flamegraph/frame_animator.go b/internal/tui/flamegraph/frame_animator.go
index 26755e8..86e52ec 100644
--- a/internal/tui/flamegraph/frame_animator.go
+++ b/internal/tui/flamegraph/frame_animator.go
@@ -82,7 +82,7 @@ func (fa *FrameAnimator) reset() {
// frameIndexAt returns the index of the frame rendered at terminal coordinates
// (x, y), or -1 if no frame occupies that cell. showHelp adds one extra line
// to the UI chrome so the frame area row calculations account for it.
-func frameIndexAt(frames []tuiFrame, x, y, width, height int, showHelp bool) int {
+func frameIndexAt(frames []tuiFrame, x, y, width, height int, showHelp, heightMetricActive bool) int {
if len(frames) == 0 || width <= 0 || height <= 0 {
return -1
}
@@ -105,7 +105,7 @@ func frameIndexAt(frames []tuiFrame, x, y, width, height int, showHelp bool) int
if y < 1 || y > availableRows {
return -1
}
- targetRow := frameCoordToTargetRow(frames, y-1, availableRows)
+ targetRow := frameCoordToTargetRow(frames, y-1, availableRows, heightMetricActive)
if targetRow < 0 {
return -1
}
@@ -115,10 +115,15 @@ func frameIndexAt(frames []tuiFrame, x, y, width, height int, showHelp bool) int
// frameCoordToTargetRow converts a data-area row offset (0-based, after
// stripping the toolbar row) into the logical frame row index. Returns -1 when
// the coordinate falls in the top padding above the first visible row.
-func frameCoordToTargetRow(frames []tuiFrame, dataRow, availableRows int) int {
+func frameCoordToTargetRow(frames []tuiFrame, dataRow, availableRows int, heightMetricActive bool) int {
maxRow := maxFrameRowForSet(frames, nil)
barHeight := computeBarHeight(availableRows, maxRow+1, maxBarVisualHeight)
+ leafBarHeight := barHeight
visibleDepthRows := availableRows / barHeight
+ if heightMetricActive {
+ barHeight = 1
+ visibleDepthRows = availableRows
+ }
if visibleDepthRows < 1 {
visibleDepthRows = 1
}
@@ -126,7 +131,17 @@ func frameCoordToTargetRow(frames []tuiFrame, dataRow, availableRows int) int {
if maxRow+1 > visibleDepthRows {
rowOffset = maxRow + 1 - visibleDepthRows
}
+ if heightMetricActive {
+ visibleNonLeafRows := max(0, maxRow-rowOffset)
+ leafBarHeight = availableRows - visibleNonLeafRows
+ if leafBarHeight < 1 {
+ leafBarHeight = 1
+ }
+ }
renderedRows := (maxRow - rowOffset + 1) * barHeight
+ if heightMetricActive {
+ renderedRows = leafBarHeight + max(0, maxRow-rowOffset)*barHeight
+ }
padTop := 0
if renderedRows < availableRows {
padTop = availableRows - renderedRows
@@ -134,8 +149,18 @@ func frameCoordToTargetRow(frames []tuiFrame, dataRow, availableRows int) int {
if dataRow < padTop {
return -1
}
- depthFromTop := (dataRow - padTop) / barHeight
- return maxRow - depthFromTop
+ rowInRender := dataRow - padTop
+ for row := maxRow; row >= rowOffset; row-- {
+ rowHeight := barHeight
+ if heightMetricActive && row == maxRow {
+ rowHeight = leafBarHeight
+ }
+ if rowInRender < rowHeight {
+ return row
+ }
+ rowInRender -= rowHeight
+ }
+ return -1
}
// findFrameAtRow scans frames for the narrowest one that occupies logical row
diff --git a/internal/tui/flamegraph/frame_animator_test.go b/internal/tui/flamegraph/frame_animator_test.go
new file mode 100644
index 0000000..6b4d4f7
--- /dev/null
+++ b/internal/tui/flamegraph/frame_animator_test.go
@@ -0,0 +1,48 @@
+package flamegraph
+
+import "testing"
+
+func TestFrameCoordToTargetRowKeepsUniformBarMapping(t *testing.T) {
+ frames := []tuiFrame{
+ {Name: "root", Row: 0, Col: 0, Width: 20, Path: "root"},
+ {Name: "a", Row: 1, Col: 0, Width: 20, Path: "root" + pathSeparator + "a"},
+ {Name: "b", Row: 2, Col: 0, Width: 20, Path: "root" + pathSeparator + "a" + pathSeparator + "b"},
+ {Name: "leaf", Row: 3, Col: 0, Width: 20, Path: "root" + pathSeparator + "a" + pathSeparator + "b" + pathSeparator + "leaf"},
+ }
+ availableRows := 8
+ want := []int{3, 3, 2, 2, 1, 1, 0, 0}
+ for dataRow, expected := range want {
+ if got := frameCoordToTargetRow(frames, dataRow, availableRows, false); got != expected {
+ t.Fatalf("dataRow=%d: got row=%d want=%d", dataRow, got, expected)
+ }
+ }
+}
+
+func TestFrameCoordToTargetRowHeightMetricMapsExpandedLeafBand(t *testing.T) {
+ frames := []tuiFrame{
+ {Name: "root", Row: 0, Col: 0, Width: 20, Path: "root"},
+ {Name: "leaf", Row: 1, Col: 0, Width: 20, Path: "root" + pathSeparator + "leaf", HeightTotal: 100},
+ }
+ availableRows := 6
+ want := []int{1, 1, 1, 1, 1, 0}
+ for dataRow, expected := range want {
+ if got := frameCoordToTargetRow(frames, dataRow, availableRows, true); got != expected {
+ t.Fatalf("dataRow=%d: got row=%d want=%d", dataRow, got, expected)
+ }
+ }
+}
+
+func TestFrameIndexAtHeightMetricMapsClicksInExpandedLeafBand(t *testing.T) {
+ frames := []tuiFrame{
+ {Name: "root", Row: 0, Col: 0, Width: 20, Path: "root"},
+ {Name: "leaf", Row: 1, Col: 0, Width: 20, Path: "root" + pathSeparator + "leaf", HeightTotal: 100},
+ }
+ for y := 1; y <= 5; y++ {
+ if got := frameIndexAt(frames, 10, y, 20, 9, false, true); got != 1 {
+ t.Fatalf("y=%d: expected leaf frame index 1, got %d", y, got)
+ }
+ }
+ if got := frameIndexAt(frames, 10, 6, 20, 9, false, true); got != 0 {
+ t.Fatalf("y=6: expected root frame index 0, got %d", got)
+ }
+}
diff --git a/internal/tui/flamegraph/model.go b/internal/tui/flamegraph/model.go
index fe9b73b..81fcb7b 100644
--- a/internal/tui/flamegraph/model.go
+++ b/internal/tui/flamegraph/model.go
@@ -1088,12 +1088,12 @@ func (m Model) rootSnapshotPath() string {
// frameIndexAt delegates to the FrameAnimator package-level helper to convert
// terminal coordinates (x, y) to a frame index, accounting for UI chrome.
func (m Model) frameIndexAt(x, y int) int {
- return frameIndexAt(m.frames, x, y, m.width, m.height, m.showHelp)
+ return frameIndexAt(m.frames, x, y, m.width, m.height, m.showHelp, strings.TrimSpace(m.heightField) != "")
}
// frameCoordToTargetRow delegates to the FrameAnimator package-level helper.
func (m Model) frameCoordToTargetRow(dataRow, availableRows int) int {
- return frameCoordToTargetRow(m.frames, dataRow, availableRows)
+ return frameCoordToTargetRow(m.frames, dataRow, availableRows, strings.TrimSpace(m.heightField) != "")
}
func (m Model) withZoomLineage(frames []tuiFrame) []tuiFrame {