detached 181a3c8

fix(render): split every shared separator stretch and close lit corners

commit
181a3c81b2b3e416997d6a2f3f1924fe08a8cef7
author
brice · · 2026-09-26 15:00 UTC
committer
brice · · 2026-09-26 15:07 UTC
parent
eba6a62
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +100 -27
M internal/adapters/vulkan/renderer.go +77 -27
@@ -458,29 +458,31 @@ func (r *Renderer) draws(s ports.Scene, contents map[ports.WindowID]ports.Surfac
458458 }
459459 // The focused window lights the lines along its own sides only: its
460460 // inset border, and its neighbors' border line where they own it.
461- // With exactly two tiles one line splits them: like tmux, the focused
462- // window lights only its half of it (left/top window: first half).
461+ // tmux style: each stretch of line shared by two tiles is split in
462+ // half, the left or top window owning the first half. The focused
463+ // window lights its half of every stretch along its sides.
463464 if focused != nil && s.Border.Active != "" && s.Border.Width > 0 {
464- f, tiles := focused, 0
465- for _, w := range s.Windows {
466- if !w.Hidden && !w.Popup && w.Neighbors != 0 {
467- tiles++
468- }
465+ f := focused
466+ // Inset sides with no neighbor (floats) light fully.
467+ for _, strip := range borderStrips(f.Rect, f.Inset&^f.Neighbors, 0, s.Border.Width) {
468+ add(physRect(strip.X, strip.Y, strip.W, strip.H), parseColor(s.Border.Active))
469469 }
470470 for _, side := range []ports.Sides{ports.SideLeft, ports.SideRight, ports.SideTop, ports.SideBottom} {
471- out := 0
472- switch {
473- case f.Inset&side != 0:
474- case f.Neighbors&side != 0:
475- out = s.Border.Width
476- default:
471+ if f.Neighbors&side == 0 {
477472 continue
478473 }
479- for _, strip := range borderStrips(f.Rect, side, out, s.Border.Width) {
480- if tiles == 2 {
481- strip = halfStrip(strip, side)
474+ out := s.Border.Width
475+ if f.Inset&side != 0 {
476+ out = 0
477+ }
478+ line := borderStrips(f.Rect, side, out, s.Border.Width)[0]
479+ for _, n := range s.Windows {
480+ if n.ID == f.ID || n.Hidden || n.Popup || n.Neighbors&opposite(side) == 0 {
481+ continue
482+ }
483+ if part, ok := focusedHalf(line, *f, n.Rect, side, s.Border.Width); ok {
484+ add(physRect(part.X, part.Y, part.W, part.H), parseColor(s.Border.Active))
482485 }
483- add(physRect(strip.X, strip.Y, strip.W, strip.H), parseColor(s.Border.Active))
484486 }
485487 }
486488 }
@@ -639,18 +641,66 @@ func borderStrips(r ports.Rect, sides ports.Sides, out, b int) []ports.Rect {
639641 return strips
640642 }
641643
642-// halfStrip keeps the half of a separator strip owned by the window on
643-// side of it: the first half for the left or top window, else the second.
644-func halfStrip(r ports.Rect, side ports.Sides) ports.Rect {
644+// opposite is the side facing side.
645+func opposite(side ports.Sides) ports.Sides {
645646 switch side {
646- case ports.SideRight:
647- r.H /= 2
648647 case ports.SideLeft:
649- r.Y, r.H = r.Y+r.H/2, r.H-r.H/2
650- case ports.SideBottom:
651- r.W /= 2
648+ return ports.SideRight
649+ case ports.SideRight:
650+ return ports.SideLeft
652651 case ports.SideTop:
653- r.X, r.W = r.X+r.W/2, r.W-r.W/2
652+ return ports.SideBottom
653+ }
654+ return ports.SideTop
655+}
656+
657+// focusedHalf is the part of line, along side of window w, that w owns
658+// in the stretch it shares with neighbor n: the first half when w is the
659+// left or top window, else the second. ok is false when n is not across
660+// side or they share no stretch. A part reaching a corner of w whose
661+// crossing line lies outside w (a neighbor's) runs b further to close
662+// the corner.
663+func focusedHalf(line ports.Rect, w ports.SceneWindow, n ports.Rect, side ports.Sides, b int) (ports.Rect, bool) {
664+ f := w.Rect
665+ outside := func(s ports.Sides) bool { return w.Neighbors&s != 0 && w.Inset&s == 0 }
666+ var lo, hi int
667+ switch side {
668+ case ports.SideRight, ports.SideLeft:
669+ if side == ports.SideRight && n.X < f.X+f.W || side == ports.SideLeft && n.X+n.W > f.X {
670+ return ports.Rect{}, false
671+ }
672+ lo, hi = max(f.Y, n.Y), min(f.Y+f.H, n.Y+n.H)
673+ default:
674+ if side == ports.SideBottom && n.Y < f.Y+f.H || side == ports.SideTop && n.Y+n.H > f.Y {
675+ return ports.Rect{}, false
676+ }
677+ lo, hi = max(f.X, n.X), min(f.X+f.W, n.X+n.W)
678+ }
679+ if hi <= lo {
680+ return ports.Rect{}, false
681+ }
682+ mid := lo + (hi-lo)/2
683+ if side == ports.SideLeft || side == ports.SideTop {
684+ lo = mid
685+ } else {
686+ hi = mid
687+ }
688+ if side == ports.SideRight || side == ports.SideLeft {
689+ if lo == f.Y && outside(ports.SideTop) {
690+ lo -= b
691+ }
692+ if hi == f.Y+f.H && outside(ports.SideBottom) {
693+ hi += b
694+ }
695+ line.Y, line.H = lo, hi-lo
696+ } else {
697+ if lo == f.X && outside(ports.SideLeft) {
698+ lo -= b
699+ }
700+ if hi == f.X+f.W && outside(ports.SideRight) {
701+ hi += b
702+ }
703+ line.X, line.W = lo, hi-lo
654704 }
655- return r
705+ return line, true
656706 }
@@ -458,29 +458,31 @@ func (r *Renderer) draws(s ports.Scene, contents map[ports.WindowID]ports.Surfac
458 }458 }
459 // The focused window lights the lines along its own sides only: its459 // The focused window lights the lines along its own sides only: its
460 // inset border, and its neighbors' border line where they own it.460 // inset border, and its neighbors' border line where they own it.
461- // With exactly two tiles one line splits them: like tmux, the focused461+ // tmux style: each stretch of line shared by two tiles is split in
462- // window lights only its half of it (left/top window: first half).462+ // half, the left or top window owning the first half. The focused
463+ // window lights its half of every stretch along its sides.
463 if focused != nil && s.Border.Active != "" && s.Border.Width > 0 {464 if focused != nil && s.Border.Active != "" && s.Border.Width > 0 {
464- f, tiles := focused, 0465+ f := focused
465- for _, w := range s.Windows {466+ // Inset sides with no neighbor (floats) light fully.
466- if !w.Hidden && !w.Popup && w.Neighbors != 0 {467+ for _, strip := range borderStrips(f.Rect, f.Inset&^f.Neighbors, 0, s.Border.Width) {
467- tiles++468+ add(physRect(strip.X, strip.Y, strip.W, strip.H), parseColor(s.Border.Active))
468- }
469 }469 }
470 for _, side := range []ports.Sides{ports.SideLeft, ports.SideRight, ports.SideTop, ports.SideBottom} {470 for _, side := range []ports.Sides{ports.SideLeft, ports.SideRight, ports.SideTop, ports.SideBottom} {
471- out := 0471+ if f.Neighbors&side == 0 {
472- switch {
473- case f.Inset&side != 0:
474- case f.Neighbors&side != 0:
475- out = s.Border.Width
476- default:
477 continue472 continue
478 }473 }
479- for _, strip := range borderStrips(f.Rect, side, out, s.Border.Width) {474+ out := s.Border.Width
480- if tiles == 2 {475+ if f.Inset&side != 0 {
481- strip = halfStrip(strip, side)476+ out = 0
477+ }
478+ line := borderStrips(f.Rect, side, out, s.Border.Width)[0]
479+ for _, n := range s.Windows {
480+ if n.ID == f.ID || n.Hidden || n.Popup || n.Neighbors&opposite(side) == 0 {
481+ continue
482+ }
483+ if part, ok := focusedHalf(line, *f, n.Rect, side, s.Border.Width); ok {
484+ add(physRect(part.X, part.Y, part.W, part.H), parseColor(s.Border.Active))
482 }485 }
483- add(physRect(strip.X, strip.Y, strip.W, strip.H), parseColor(s.Border.Active))
484 }486 }
485 }487 }
486 }488 }
@@ -639,18 +641,66 @@ func borderStrips(r ports.Rect, sides ports.Sides, out, b int) []ports.Rect {
639 return strips641 return strips
640 }642 }
641 643
642-// halfStrip keeps the half of a separator strip owned by the window on644+// opposite is the side facing side.
643-// side of it: the first half for the left or top window, else the second.645+func opposite(side ports.Sides) ports.Sides {
644-func halfStrip(r ports.Rect, side ports.Sides) ports.Rect {
645 switch side {646 switch side {
646- case ports.SideRight:
647- r.H /= 2
648 case ports.SideLeft:647 case ports.SideLeft:
649- r.Y, r.H = r.Y+r.H/2, r.H-r.H/2648+ return ports.SideRight
650- case ports.SideBottom:649+ case ports.SideRight:
651- r.W /= 2650+ return ports.SideLeft
652 case ports.SideTop:651 case ports.SideTop:
653- r.X, r.W = r.X+r.W/2, r.W-r.W/2652+ return ports.SideBottom
653+ }
654+ return ports.SideTop
655+}
656+
657+// focusedHalf is the part of line, along side of window w, that w owns
658+// in the stretch it shares with neighbor n: the first half when w is the
659+// left or top window, else the second. ok is false when n is not across
660+// side or they share no stretch. A part reaching a corner of w whose
661+// crossing line lies outside w (a neighbor's) runs b further to close
662+// the corner.
663+func focusedHalf(line ports.Rect, w ports.SceneWindow, n ports.Rect, side ports.Sides, b int) (ports.Rect, bool) {
664+ f := w.Rect
665+ outside := func(s ports.Sides) bool { return w.Neighbors&s != 0 && w.Inset&s == 0 }
666+ var lo, hi int
667+ switch side {
668+ case ports.SideRight, ports.SideLeft:
669+ if side == ports.SideRight && n.X < f.X+f.W || side == ports.SideLeft && n.X+n.W > f.X {
670+ return ports.Rect{}, false
671+ }
672+ lo, hi = max(f.Y, n.Y), min(f.Y+f.H, n.Y+n.H)
673+ default:
674+ if side == ports.SideBottom && n.Y < f.Y+f.H || side == ports.SideTop && n.Y+n.H > f.Y {
675+ return ports.Rect{}, false
676+ }
677+ lo, hi = max(f.X, n.X), min(f.X+f.W, n.X+n.W)
678+ }
679+ if hi <= lo {
680+ return ports.Rect{}, false
681+ }
682+ mid := lo + (hi-lo)/2
683+ if side == ports.SideLeft || side == ports.SideTop {
684+ lo = mid
685+ } else {
686+ hi = mid
687+ }
688+ if side == ports.SideRight || side == ports.SideLeft {
689+ if lo == f.Y && outside(ports.SideTop) {
690+ lo -= b
691+ }
692+ if hi == f.Y+f.H && outside(ports.SideBottom) {
693+ hi += b
694+ }
695+ line.Y, line.H = lo, hi-lo
696+ } else {
697+ if lo == f.X && outside(ports.SideLeft) {
698+ lo -= b
699+ }
700+ if hi == f.X+f.W && outside(ports.SideRight) {
701+ hi += b
702+ }
703+ line.X, line.W = lo, hi-lo
654 }704 }
655- return r705+ return line, true
656 }706 }
M internal/adapters/vulkan/renderer_test.go +23 -0
@@ -77,6 +77,29 @@ func TestRendererRender(t *testing.T) {
7777 check(pair, map[image.Point]color.RGBA{{14, 3}: lit, {14, 12}: gray})
7878 pair.Windows[0].Focused = false
7979 check(pair, map[image.Point]color.RGBA{{14, 5}: gray, {15, 5}: gray})
80+ // Three columns: each line is split on its own. Focus in the middle
81+ // lights the bottom half of the left line and the top half of the right one.
82+ three := s
83+ three.Windows = []ports.SceneWindow{
84+ {ID: 1, Rect: ports.Rect{X: 0, Y: 0, W: 16, H: 16}, Inset: ports.SideRight, Neighbors: ports.SideRight},
85+ {ID: 2, Rect: ports.Rect{X: 16, Y: 0, W: 16, H: 16}, Inset: ports.SideRight, Neighbors: ports.SideLeft | ports.SideRight, Focused: true},
86+ {ID: 3, Rect: ports.Rect{X: 32, Y: 0, W: 16, H: 16}, Neighbors: ports.SideLeft},
87+ }
88+ check(three, map[image.Point]color.RGBA{{14, 3}: gray, {14, 12}: lit, {30, 3}: lit, {30, 12}: gray})
89+ // Bottom-right tile of a split column: its top line (owned by the tile
90+ // above) meets the left line (owned by the left column); lit halves
91+ // ending at that corner close it.
92+ grid := s
93+ grid.Windows = []ports.SceneWindow{
94+ {ID: 1, Rect: ports.Rect{X: 0, Y: 0, W: 16, H: 32}, Inset: ports.SideRight, Neighbors: ports.SideRight},
95+ {ID: 2, Rect: ports.Rect{X: 16, Y: 0, W: 16, H: 16}, Inset: ports.SideBottom, Neighbors: ports.SideLeft | ports.SideBottom},
96+ {ID: 3, Rect: ports.Rect{X: 16, Y: 16, W: 16, H: 16}, Neighbors: ports.SideLeft | ports.SideTop, Focused: true},
97+ }
98+ check(grid, map[image.Point]color.RGBA{{14, 30}: lit, {14, 20}: gray, {24, 14}: lit, {17, 14}: gray})
99+ grid.Windows[2].Focused, grid.Windows[1].Focused = false, true
100+ // Top-right tile: left line bottom half and bottom line left half meet
101+ // at the corner inside the left column's line.
102+ check(grid, map[image.Point]color.RGBA{{14, 14}: lit, {14, 15}: lit, {16, 14}: lit, {14, 3}: gray, {30, 14}: gray})
80103 s.Windows[0].Fullscreen = true
81104 check(s, map[image.Point]color.RGBA{{9, 9}: wc})
82105 s.Windows[0].Fullscreen = false
@@ -77,6 +77,29 @@ func TestRendererRender(t *testing.T) {
77 check(pair, map[image.Point]color.RGBA{{14, 3}: lit, {14, 12}: gray})77 check(pair, map[image.Point]color.RGBA{{14, 3}: lit, {14, 12}: gray})
78 pair.Windows[0].Focused = false78 pair.Windows[0].Focused = false
79 check(pair, map[image.Point]color.RGBA{{14, 5}: gray, {15, 5}: gray})79 check(pair, map[image.Point]color.RGBA{{14, 5}: gray, {15, 5}: gray})
80+ // Three columns: each line is split on its own. Focus in the middle
81+ // lights the bottom half of the left line and the top half of the right one.
82+ three := s
83+ three.Windows = []ports.SceneWindow{
84+ {ID: 1, Rect: ports.Rect{X: 0, Y: 0, W: 16, H: 16}, Inset: ports.SideRight, Neighbors: ports.SideRight},
85+ {ID: 2, Rect: ports.Rect{X: 16, Y: 0, W: 16, H: 16}, Inset: ports.SideRight, Neighbors: ports.SideLeft | ports.SideRight, Focused: true},
86+ {ID: 3, Rect: ports.Rect{X: 32, Y: 0, W: 16, H: 16}, Neighbors: ports.SideLeft},
87+ }
88+ check(three, map[image.Point]color.RGBA{{14, 3}: gray, {14, 12}: lit, {30, 3}: lit, {30, 12}: gray})
89+ // Bottom-right tile of a split column: its top line (owned by the tile
90+ // above) meets the left line (owned by the left column); lit halves
91+ // ending at that corner close it.
92+ grid := s
93+ grid.Windows = []ports.SceneWindow{
94+ {ID: 1, Rect: ports.Rect{X: 0, Y: 0, W: 16, H: 32}, Inset: ports.SideRight, Neighbors: ports.SideRight},
95+ {ID: 2, Rect: ports.Rect{X: 16, Y: 0, W: 16, H: 16}, Inset: ports.SideBottom, Neighbors: ports.SideLeft | ports.SideBottom},
96+ {ID: 3, Rect: ports.Rect{X: 16, Y: 16, W: 16, H: 16}, Neighbors: ports.SideLeft | ports.SideTop, Focused: true},
97+ }
98+ check(grid, map[image.Point]color.RGBA{{14, 30}: lit, {14, 20}: gray, {24, 14}: lit, {17, 14}: gray})
99+ grid.Windows[2].Focused, grid.Windows[1].Focused = false, true
100+ // Top-right tile: left line bottom half and bottom line left half meet
101+ // at the corner inside the left column's line.
102+ check(grid, map[image.Point]color.RGBA{{14, 14}: lit, {14, 15}: lit, {16, 14}: lit, {14, 3}: gray, {30, 14}: gray})
80 s.Windows[0].Fullscreen = true103 s.Windows[0].Fullscreen = true
81 check(s, map[image.Point]color.RGBA{{9, 9}: wc})104 check(s, map[image.Point]color.RGBA{{9, 9}: wc})
82 s.Windows[0].Fullscreen = false105 s.Windows[0].Fullscreen = false

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