fix(vulkan): anchor hint labels outside the target corner
- commit
- 853fbd998c34eb07488eb1de49d689377f433843
- author
- brice · · 2026-10-19 13:45 UTC
- parent
- 5f783b0
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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 | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "image" | |
| 5 | + | "math" | |
| 5 | 6 | ||
| 6 | 7 | "github.com/bnema/neferwl/internal/adapters/hintlabel" | |
| 7 | 8 | ) | |
| @@ -24,9 +25,11 @@ func (w *sceneWalk) hintLabels() { | |||
| 24 | 25 | if lw <= 0 || lh <= 0 { | |
| 25 | 26 | continue | |
| 26 | 27 | } | |
| 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)) | |
| 30 | 33 | rect := image.Rect(x, y, x+lw, y+lh) | |
| 31 | 34 | if rect.Intersect(w.bounds).Empty() { | |
| 32 | 35 | 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 | continue | 26 | 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 | continue | 35 | continue | ||
M internal/adapters/vulkan/hint_labels_test.go +27 -5
| @@ -2,6 +2,7 @@ package vulkan | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "image" | |
| 5 | + | "math" | |
| 5 | 6 | "testing" | |
| 6 | 7 | ||
| 7 | 8 | "github.com/stretchr/testify/require" | |
| @@ -19,19 +20,21 @@ func TestHintLabelsDrawnAfterIndicators(t *testing.T) { | |||
| 19 | 20 | s := ports.Scene{Scale: float64(scale), | |
| 20 | 21 | CaptureIndicators: []ports.CaptureIndicator{{Rect: ports.Rect{W: 64, H: 64}}}, | |
| 21 | 22 | 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}, | |
| 24 | 25 | }} | |
| 25 | 26 | bare := s | |
| 26 | 27 | bare.HintLabels = nil | |
| 27 | 28 | before := len(r.draws(bare, nil, newDamage(&target{}, bare, image.Rect(0, 0, 256, 256)))) | |
| 28 | 29 | ds := r.draws(s, nil, newDamage(&target{}, s, image.Rect(0, 0, 256, 256))) | |
| 29 | 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 | 32 | d := ds[before+i] | |
| 32 | 33 | 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]) | |
| 35 | 38 | require.Greater(t, d.pc.rect[2], d.pc.rect[0]) | |
| 36 | 39 | require.Greater(t, d.pc.rect[3], d.pc.rect[1]) | |
| 37 | 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 | 64 | func TestHintLabelsAbsentDrawNothing(t *testing.T) { | |
| 43 | 65 | r := &Renderer{width: 64, height: 64} | |
| 44 | 66 | 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 := s | 26 | bare := s | ||
| 26 | bare.HintLabels = nil | 27 | 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} | ||