fix(selection): place output details at the top right
- commit
- 111a4b4cb837d0b22e4d697e962ea9ae99c6c749
- author
- brice · · 2026-09-30 13:51 UTC
- committer
- brice · · 2026-09-30 14:48 UTC
- parent
- 2c1e060
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +30 -3
M internal/adapters/selection/readability_test.go +29 -2
| @@ -71,11 +71,38 @@ func TestWorkspaceLabelAvoidsOutputHeader(t *testing.T) { | |||
| 71 | 71 | m.input(key("w")) | |
| 72 | 72 | s := m.scene() | |
| 73 | 73 | assert.Equal(t, "640 × 360", m.label.text) | |
| 74 | - | assert.GreaterOrEqual(t, s.label.y, s.header.y+s.header.h) | |
| 75 | - | assert.GreaterOrEqual(t, s.label.y, 60.0, "label falls inside the workspace") | |
| 74 | + | assert.True(t, s.label.x+s.label.w <= s.header.x || s.label.x >= s.header.x+s.header.w || | |
| 75 | + | s.label.y+s.label.h <= s.header.y || s.label.y >= s.header.y+s.header.h, "label and header must not overlap") | |
| 76 | 76 | assert.LessOrEqual(t, s.label.y+s.label.h, 480.0) | |
| 77 | 77 | } | |
| 78 | 78 | ||
| 79 | + | func TestRightWorkspaceLabelAvoidsOutputHeader(t *testing.T) { | |
| 80 | + | m, _ := newBareModel() | |
| 81 | + | m.setWorkspaces([]ports.Workspace{{ID: 11, OutputID: 7, Active: true, Region: ports.Region{X: 470, Y: 60, Width: 170, Height: 360}}}) | |
| 82 | + | m.resize(640, 480, 1) | |
| 83 | + | m.input(key("w")) | |
| 84 | + | s := m.scene() | |
| 85 | + | assert.Greater(t, s.label.x+s.label.w, s.header.x, "label shares the header's horizontal range") | |
| 86 | + | assert.Less(t, s.label.x, s.header.x+s.header.w) | |
| 87 | + | assert.GreaterOrEqual(t, s.label.y, s.header.y+s.header.h, "collision moves dimensions below the header") | |
| 88 | + | assert.GreaterOrEqual(t, s.label.y, 60.0, "dimensions move inside the workspace") | |
| 89 | + | } | |
| 90 | + | ||
| 91 | + | func TestOutputHeaderAtTopRight(t *testing.T) { | |
| 92 | + | for _, width := range []int{1920, 640, 200, 20} { | |
| 93 | + | m := newTestModel(t) | |
| 94 | + | m.resize(width, 480, 1) | |
| 95 | + | s := m.scene() | |
| 96 | + | wantRightGap := min(margin, float64(width)-s.header.w) | |
| 97 | + | assert.Equal(t, float64(width)-wantRightGap, s.header.x+s.header.w) | |
| 98 | + | assert.GreaterOrEqual(t, s.header.x, 0.0) | |
| 99 | + | } | |
| 100 | + | m := newTestModel(t) | |
| 101 | + | m.input(key("m")) | |
| 102 | + | s := m.scene() | |
| 103 | + | assert.Less(t, s.label.x+s.label.w, s.header.x, "monitor dimensions stay left of the output details") | |
| 104 | + | } | |
| 105 | + | ||
| 79 | 106 | func TestFooterUnchangedAllocations(t *testing.T) { | |
| 80 | 107 | if raceEnabled { | |
| 81 | 108 | t.Skip("the race detector adds allocations") | |
| @@ -71,11 +71,38 @@ func TestWorkspaceLabelAvoidsOutputHeader(t *testing.T) { | |||||
| 71 | m.input(key("w")) | 71 | m.input(key("w")) | ||
| 72 | s := m.scene() | 72 | s := m.scene() | ||
| 73 | assert.Equal(t, "640 × 360", m.label.text) | 73 | assert.Equal(t, "640 × 360", m.label.text) | ||
| 74 | - | assert.GreaterOrEqual(t, s.label.y, s.header.y+s.header.h) | 74 | + | assert.True(t, s.label.x+s.label.w <= s.header.x || s.label.x >= s.header.x+s.header.w || |
| 75 | - | assert.GreaterOrEqual(t, s.label.y, 60.0, "label falls inside the workspace") | 75 | + | s.label.y+s.label.h <= s.header.y || s.label.y >= s.header.y+s.header.h, "label and header must not overlap") |
| 76 | assert.LessOrEqual(t, s.label.y+s.label.h, 480.0) | 76 | assert.LessOrEqual(t, s.label.y+s.label.h, 480.0) | ||
| 77 | } | 77 | } | ||
| 78 | 78 | ||||
| 79 | + | func TestRightWorkspaceLabelAvoidsOutputHeader(t *testing.T) { | |||
| 80 | + | m, _ := newBareModel() | |||
| 81 | + | m.setWorkspaces([]ports.Workspace{{ID: 11, OutputID: 7, Active: true, Region: ports.Region{X: 470, Y: 60, Width: 170, Height: 360}}}) | |||
| 82 | + | m.resize(640, 480, 1) | |||
| 83 | + | m.input(key("w")) | |||
| 84 | + | s := m.scene() | |||
| 85 | + | assert.Greater(t, s.label.x+s.label.w, s.header.x, "label shares the header's horizontal range") | |||
| 86 | + | assert.Less(t, s.label.x, s.header.x+s.header.w) | |||
| 87 | + | assert.GreaterOrEqual(t, s.label.y, s.header.y+s.header.h, "collision moves dimensions below the header") | |||
| 88 | + | assert.GreaterOrEqual(t, s.label.y, 60.0, "dimensions move inside the workspace") | |||
| 89 | + | } | |||
| 90 | + | ||||
| 91 | + | func TestOutputHeaderAtTopRight(t *testing.T) { | |||
| 92 | + | for _, width := range []int{1920, 640, 200, 20} { | |||
| 93 | + | m := newTestModel(t) | |||
| 94 | + | m.resize(width, 480, 1) | |||
| 95 | + | s := m.scene() | |||
| 96 | + | wantRightGap := min(margin, float64(width)-s.header.w) | |||
| 97 | + | assert.Equal(t, float64(width)-wantRightGap, s.header.x+s.header.w) | |||
| 98 | + | assert.GreaterOrEqual(t, s.header.x, 0.0) | |||
| 99 | + | } | |||
| 100 | + | m := newTestModel(t) | |||
| 101 | + | m.input(key("m")) | |||
| 102 | + | s := m.scene() | |||
| 103 | + | assert.Less(t, s.label.x+s.label.w, s.header.x, "monitor dimensions stay left of the output details") | |||
| 104 | + | } | |||
| 105 | + | ||||
| 79 | func TestFooterUnchangedAllocations(t *testing.T) { | 106 | func TestFooterUnchangedAllocations(t *testing.T) { | ||
| 80 | if raceEnabled { | 107 | if raceEnabled { | ||
| 81 | t.Skip("the race detector adds allocations") | 108 | t.Skip("the race detector adds allocations") | ||
M internal/adapters/selection/scene.go +1 -1
| @@ -104,7 +104,7 @@ func (m *model) scene() scene { | |||
| 104 | 104 | s.label = labelRect(fr, w, h) | |
| 105 | 105 | } | |
| 106 | 106 | hw, hh := min(headerWidth(m.header), w), min(headerH, h) | |
| 107 | - | s.header = frect{min(margin, max(w-hw, 0)), min(margin, max(h-hh, 0)), hw, hh} | |
| 107 | + | s.header = frect{max(w-hw-margin, 0), min(margin, max(h-hh, 0)), hw, hh} | |
| 108 | 108 | // Keep workspace dimensions clear of the output header; region and | |
| 109 | 109 | // monitor labels retain their independent placement. | |
| 110 | 110 | if m.picker.Kind() == core.PickWorkspace && s.label.w > 0 && | |
| @@ -104,7 +104,7 @@ func (m *model) scene() scene { | |||||
| 104 | s.label = labelRect(fr, w, h) | 104 | s.label = labelRect(fr, w, h) | ||
| 105 | } | 105 | } | ||
| 106 | hw, hh := min(headerWidth(m.header), w), min(headerH, h) | 106 | hw, hh := min(headerWidth(m.header), w), min(headerH, h) | ||
| 107 | - | s.header = frect{min(margin, max(w-hw, 0)), min(margin, max(h-hh, 0)), hw, hh} | 107 | + | s.header = frect{max(w-hw-margin, 0), min(margin, max(h-hh, 0)), hw, hh} |
| 108 | // Keep workspace dimensions clear of the output header; region and | 108 | // Keep workspace dimensions clear of the output header; region and | ||
| 109 | // monitor labels retain their independent placement. | 109 | // monitor labels retain their independent placement. | ||
| 110 | if m.picker.Kind() == core.PickWorkspace && s.label.w > 0 && | 110 | if m.picker.Kind() == core.PickWorkspace && s.label.w > 0 && | ||