detached 853fbd9

fix(vulkan): anchor hint labels outside the target corner

commit
853fbd998c34eb07488eb1de49d689377f433843
author
brice · · 2026-10-19 13:45 UTC
parent
5f783b0
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +33 -8
M internal/adapters/vulkan/hint_labels.go +6 -3
@@ -2,6 +2,7 @@ package vulkan
22
33 import (
44 "image"
5+ "math"
56
67 "github.com/bnema/neferwl/internal/adapters/hintlabel"
78 )
@@ -24,9 +25,11 @@ func (w *sceneWalk) hintLabels() {
2425 if lw <= 0 || lh <= 0 {
2526 continue
2627 }
27- // Inside the output, whatever the target's position.
28- x := min(max(w.phys(l.Rect.X), w.bounds.Min.X), max(w.bounds.Max.X-lw, w.bounds.Min.X))
29- y := min(max(w.phys(l.Rect.Y), w.bounds.Min.Y), max(w.bounds.Max.Y-lh, w.bounds.Min.Y))
28+ // Anchored outside the target's top-left corner, overlapping it by a
29+ // quarter of the label height, then kept inside the output.
30+ inset := int(math.Round(0.25 * float64(lh)))
31+ x := min(max(w.phys(l.Rect.X)-lw+inset, w.bounds.Min.X), max(w.bounds.Max.X-lw, w.bounds.Min.X))
32+ y := min(max(w.phys(l.Rect.Y)-lh+inset, w.bounds.Min.Y), max(w.bounds.Max.Y-lh, w.bounds.Min.Y))
3033 rect := image.Rect(x, y, x+lw, y+lh)
3134 if rect.Intersect(w.bounds).Empty() {
3235 continue
@@ -2,6 +2,7 @@ package vulkan
2 2
3 import (3 import (
4 "image"4 "image"
5+ "math"
5 6
6 "github.com/bnema/neferwl/internal/adapters/hintlabel"7 "github.com/bnema/neferwl/internal/adapters/hintlabel"
7 )8 )
@@ -24,9 +25,11 @@ func (w *sceneWalk) hintLabels() {
24 if lw <= 0 || lh <= 0 {25 if lw <= 0 || lh <= 0 {
25 continue26 continue
26 }27 }
27- // Inside the output, whatever the target's position.28+ // Anchored outside the target's top-left corner, overlapping it by a
28- x := min(max(w.phys(l.Rect.X), w.bounds.Min.X), max(w.bounds.Max.X-lw, w.bounds.Min.X))29+ // quarter of the label height, then kept inside the output.
29- y := min(max(w.phys(l.Rect.Y), w.bounds.Min.Y), max(w.bounds.Max.Y-lh, w.bounds.Min.Y))30+ inset := int(math.Round(0.25 * float64(lh)))
31+ x := min(max(w.phys(l.Rect.X)-lw+inset, w.bounds.Min.X), max(w.bounds.Max.X-lw, w.bounds.Min.X))
32+ y := min(max(w.phys(l.Rect.Y)-lh+inset, w.bounds.Min.Y), max(w.bounds.Max.Y-lh, w.bounds.Min.Y))
30 rect := image.Rect(x, y, x+lw, y+lh)33 rect := image.Rect(x, y, x+lw, y+lh)
31 if rect.Intersect(w.bounds).Empty() {34 if rect.Intersect(w.bounds).Empty() {
32 continue35 continue
M internal/adapters/vulkan/hint_labels_test.go +27 -5
@@ -2,6 +2,7 @@ package vulkan
22
33 import (
44 "image"
5+ "math"
56 "testing"
67
78 "github.com/stretchr/testify/require"
@@ -19,19 +20,21 @@ func TestHintLabelsDrawnAfterIndicators(t *testing.T) {
1920 s := ports.Scene{Scale: float64(scale),
2021 CaptureIndicators: []ports.CaptureIndicator{{Rect: ports.Rect{W: 64, H: 64}}},
2122 HintLabels: []ports.HintLabel{
22- {Rect: ports.Rect{X: 10, Y: 12}, Text: "f"},
23- {Rect: ports.Rect{X: 30, Y: 40}, Text: "jk", Typed: 1},
23+ {Rect: ports.Rect{X: 80, Y: 90}, Text: "f"},
24+ {Rect: ports.Rect{X: 100, Y: 110}, Text: "jk", Typed: 1},
2425 }}
2526 bare := s
2627 bare.HintLabels = nil
2728 before := len(r.draws(bare, nil, newDamage(&target{}, bare, image.Rect(0, 0, 256, 256))))
2829 ds := r.draws(s, nil, newDamage(&target{}, s, image.Rect(0, 0, 256, 256)))
2930 require.Len(t, ds, before+2, "one draw per label, after the indicator's")
30- for i, want := range []image.Point{{10 * scale, 12 * scale}, {30 * scale, 40 * scale}} {
31+ for i, tl := range []image.Point{{80 * scale, 90 * scale}, {100 * scale, 110 * scale}} {
3132 d := ds[before+i]
3233 require.NotNil(t, d.shm)
33- require.Equal(t, int32(want.X), d.pc.rect[0])
34- require.Equal(t, int32(want.Y), d.pc.rect[1])
34+ lw, lh := int(d.pc.rect[2]-d.pc.rect[0]), int(d.pc.rect[3]-d.pc.rect[1])
35+ inset := int(math.Round(0.25 * float64(lh)))
36+ require.Equal(t, int32(tl.X-lw+inset), d.pc.rect[0])
37+ require.Equal(t, int32(tl.Y-lh+inset), d.pc.rect[1])
3538 require.Greater(t, d.pc.rect[2], d.pc.rect[0])
3639 require.Greater(t, d.pc.rect[3], d.pc.rect[1])
3740 require.Equal(t, float32(1), d.pc.color[3])
@@ -39,6 +42,25 @@ func TestHintLabelsDrawnAfterIndicators(t *testing.T) {
3942 }
4043 }
4144
45+func TestHintLabelsAnchoredAndClampedAtTopLeft(t *testing.T) {
46+ r, err := New(128, 128)
47+ if err != nil {
48+ t.Skipf("Vulkan unavailable: %v", err)
49+ }
50+ defer r.Close()
51+ // A target at the origin puts the anchor off-screen: clamped to 0,0.
52+ s := ports.Scene{Scale: 1, HintLabels: []ports.HintLabel{{Rect: ports.Rect{X: 0, Y: 0}, Text: "f"}, {Rect: ports.Rect{X: 60, Y: 60}, Text: "f"}}}
53+ ds := r.draws(s, nil, newDamage(&target{}, s, image.Rect(0, 0, 128, 128)))
54+ require.Len(t, ds, 2)
55+ require.Equal(t, int32(0), ds[0].pc.rect[0])
56+ require.Equal(t, int32(0), ds[0].pc.rect[1])
57+ lw, lh := int(ds[1].pc.rect[2]-ds[1].pc.rect[0]), int(ds[1].pc.rect[3]-ds[1].pc.rect[1])
58+ inset := int(math.Round(0.25 * float64(lh)))
59+ require.Equal(t, int32(60-lw+inset), ds[1].pc.rect[0])
60+ require.Equal(t, int32(60-lh+inset), ds[1].pc.rect[1])
61+ require.Less(t, int(ds[1].pc.rect[2]), 60+lw, "label sits left of the target, mostly outside it")
62+}
63+
4264 func TestHintLabelsAbsentDrawNothing(t *testing.T) {
4365 r := &Renderer{width: 64, height: 64}
4466 s := ports.Scene{Scale: 1}
@@ -2,6 +2,7 @@ package vulkan
2 2
3 import (3 import (
4 "image"4 "image"
5+ "math"
5 "testing"6 "testing"
6 7
7 "github.com/stretchr/testify/require"8 "github.com/stretchr/testify/require"
@@ -19,19 +20,21 @@ func TestHintLabelsDrawnAfterIndicators(t *testing.T) {
19 s := ports.Scene{Scale: float64(scale),20 s := ports.Scene{Scale: float64(scale),
20 CaptureIndicators: []ports.CaptureIndicator{{Rect: ports.Rect{W: 64, H: 64}}},21 CaptureIndicators: []ports.CaptureIndicator{{Rect: ports.Rect{W: 64, H: 64}}},
21 HintLabels: []ports.HintLabel{22 HintLabels: []ports.HintLabel{
22- {Rect: ports.Rect{X: 10, Y: 12}, Text: "f"},23+ {Rect: ports.Rect{X: 80, Y: 90}, Text: "f"},
23- {Rect: ports.Rect{X: 30, Y: 40}, Text: "jk", Typed: 1},24+ {Rect: ports.Rect{X: 100, Y: 110}, Text: "jk", Typed: 1},
24 }}25 }}
25 bare := s26 bare := s
26 bare.HintLabels = nil27 bare.HintLabels = nil
27 before := len(r.draws(bare, nil, newDamage(&target{}, bare, image.Rect(0, 0, 256, 256))))28 before := len(r.draws(bare, nil, newDamage(&target{}, bare, image.Rect(0, 0, 256, 256))))
28 ds := r.draws(s, nil, newDamage(&target{}, s, image.Rect(0, 0, 256, 256)))29 ds := r.draws(s, nil, newDamage(&target{}, s, image.Rect(0, 0, 256, 256)))
29 require.Len(t, ds, before+2, "one draw per label, after the indicator's")30 require.Len(t, ds, before+2, "one draw per label, after the indicator's")
30- for i, want := range []image.Point{{10 * scale, 12 * scale}, {30 * scale, 40 * scale}} {31+ for i, tl := range []image.Point{{80 * scale, 90 * scale}, {100 * scale, 110 * scale}} {
31 d := ds[before+i]32 d := ds[before+i]
32 require.NotNil(t, d.shm)33 require.NotNil(t, d.shm)
33- require.Equal(t, int32(want.X), d.pc.rect[0])34+ lw, lh := int(d.pc.rect[2]-d.pc.rect[0]), int(d.pc.rect[3]-d.pc.rect[1])
34- require.Equal(t, int32(want.Y), d.pc.rect[1])35+ inset := int(math.Round(0.25 * float64(lh)))
36+ require.Equal(t, int32(tl.X-lw+inset), d.pc.rect[0])
37+ require.Equal(t, int32(tl.Y-lh+inset), d.pc.rect[1])
35 require.Greater(t, d.pc.rect[2], d.pc.rect[0])38 require.Greater(t, d.pc.rect[2], d.pc.rect[0])
36 require.Greater(t, d.pc.rect[3], d.pc.rect[1])39 require.Greater(t, d.pc.rect[3], d.pc.rect[1])
37 require.Equal(t, float32(1), d.pc.color[3])40 require.Equal(t, float32(1), d.pc.color[3])
@@ -39,6 +42,25 @@ func TestHintLabelsDrawnAfterIndicators(t *testing.T) {
39 }42 }
40 }43 }
41 44
45+func TestHintLabelsAnchoredAndClampedAtTopLeft(t *testing.T) {
46+ r, err := New(128, 128)
47+ if err != nil {
48+ t.Skipf("Vulkan unavailable: %v", err)
49+ }
50+ defer r.Close()
51+ // A target at the origin puts the anchor off-screen: clamped to 0,0.
52+ s := ports.Scene{Scale: 1, HintLabels: []ports.HintLabel{{Rect: ports.Rect{X: 0, Y: 0}, Text: "f"}, {Rect: ports.Rect{X: 60, Y: 60}, Text: "f"}}}
53+ ds := r.draws(s, nil, newDamage(&target{}, s, image.Rect(0, 0, 128, 128)))
54+ require.Len(t, ds, 2)
55+ require.Equal(t, int32(0), ds[0].pc.rect[0])
56+ require.Equal(t, int32(0), ds[0].pc.rect[1])
57+ lw, lh := int(ds[1].pc.rect[2]-ds[1].pc.rect[0]), int(ds[1].pc.rect[3]-ds[1].pc.rect[1])
58+ inset := int(math.Round(0.25 * float64(lh)))
59+ require.Equal(t, int32(60-lw+inset), ds[1].pc.rect[0])
60+ require.Equal(t, int32(60-lh+inset), ds[1].pc.rect[1])
61+ require.Less(t, int(ds[1].pc.rect[2]), 60+lw, "label sits left of the target, mostly outside it")
62+}
63+
42 func TestHintLabelsAbsentDrawNothing(t *testing.T) {64 func TestHintLabelsAbsentDrawNothing(t *testing.T) {
43 r := &Renderer{width: 64, height: 64}65 r := &Renderer{width: 64, height: 64}
44 s := ports.Scene{Scale: 1}66 s := ports.Scene{Scale: 1}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 23

Tags 10

↑↓ moveenter openesc close