detached 0e47d2a

fix(render): follow the tmux rule for lit separators

commit
0e47d2a2379afec660062d05a21038733d4a6d3c
author
brice · · 2026-09-26 15:07 UTC
committer
brice · · 2026-09-26 15:07 UTC
parent
181a3c8
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +51 -77
M internal/adapters/vulkan/renderer.go +43 -67
@@ -458,11 +458,17 @@ 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- // 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.
461+ // tmux rule (screen-redraw.c): the focused window lights the lines
462+ // along its sides, corners included. With exactly two tiles a single
463+ // line splits them: each lights only its half of it (left/top window:
464+ // first half).
464465 if focused != nil && s.Border.Active != "" && s.Border.Width > 0 {
465- f := focused
466+ f, tiles := focused, 0
467+ for _, w := range s.Windows {
468+ if !w.Hidden && !w.Popup && w.Neighbors != 0 {
469+ tiles++
470+ }
471+ }
466472 // Inset sides with no neighbor (floats) light fully.
467473 for _, strip := range borderStrips(f.Rect, f.Inset&^f.Neighbors, 0, s.Border.Width) {
468474 add(physRect(strip.X, strip.Y, strip.W, strip.H), parseColor(s.Border.Active))
@@ -475,15 +481,11 @@ func (r *Renderer) draws(s ports.Scene, contents map[ports.WindowID]ports.Surfac
475481 if f.Inset&side != 0 {
476482 out = 0
477483 }
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))
485- }
484+ line := closeCorners(borderStrips(f.Rect, side, out, s.Border.Width)[0], *f, side, s.Border.Width)
485+ if tiles == 2 {
486+ line = halfStrip(line, side)
486487 }
488+ add(physRect(line.X, line.Y, line.W, line.H), parseColor(s.Border.Active))
487489 }
488490 }
489491 // Popups draw only what the client drew, shadows clipped. Window popups
@@ -641,66 +643,40 @@ func borderStrips(r ports.Rect, sides ports.Sides, out, b int) []ports.Rect {
641643 return strips
642644 }
643645
644-// opposite is the side facing side.
645-func opposite(side ports.Sides) ports.Sides {
646- switch side {
647- case ports.SideLeft:
648- return ports.SideRight
649- case ports.SideRight:
650- return ports.SideLeft
651- case ports.SideTop:
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
646+// closeCorners runs a line along side of w b further at each end where
647+// the crossing line lies outside w (a neighbor's), so lit lines meet.
648+func closeCorners(line ports.Rect, w ports.SceneWindow, side ports.Sides, b int) ports.Rect {
665649 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
650+ if side == ports.SideLeft || side == ports.SideRight {
651+ if outside(ports.SideTop) {
652+ line.Y, line.H = line.Y-b, line.H+b
671653 }
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
654+ if outside(ports.SideBottom) {
655+ line.H += b
676656 }
677- lo, hi = max(f.X, n.X), min(f.X+f.W, n.X+n.W)
657+ return line
678658 }
679- if hi <= lo {
680- return ports.Rect{}, false
659+ if outside(ports.SideLeft) {
660+ line.X, line.W = line.X-b, line.W+b
681661 }
682- mid := lo + (hi-lo)/2
683- if side == ports.SideLeft || side == ports.SideTop {
684- lo = mid
685- } else {
686- hi = mid
662+ if outside(ports.SideRight) {
663+ line.W += b
687664 }
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
665+ return line
666+}
667+
668+// halfStrip keeps the half of a separator strip owned by the window on
669+// side of it: the first half for the left or top window, else the second.
670+func halfStrip(r ports.Rect, side ports.Sides) ports.Rect {
671+ switch side {
672+ case ports.SideRight:
673+ r.H /= 2
674+ case ports.SideLeft:
675+ r.Y, r.H = r.Y+r.H/2, r.H-r.H/2
676+ case ports.SideBottom:
677+ r.W /= 2
678+ case ports.SideTop:
679+ r.X, r.W = r.X+r.W/2, r.W-r.W/2
704680 }
705- return line, true
681+ return r
706682 }
@@ -458,11 +458,17 @@ 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- // tmux style: each stretch of line shared by two tiles is split in461+ // tmux rule (screen-redraw.c): the focused window lights the lines
462- // half, the left or top window owning the first half. The focused462+ // along its sides, corners included. With exactly two tiles a single
463- // window lights its half of every stretch along its sides.463+ // line splits them: each lights only its half of it (left/top window:
464+ // first half).
464 if focused != nil && s.Border.Active != "" && s.Border.Width > 0 {465 if focused != nil && s.Border.Active != "" && s.Border.Width > 0 {
465- f := focused466+ f, tiles := focused, 0
467+ for _, w := range s.Windows {
468+ if !w.Hidden && !w.Popup && w.Neighbors != 0 {
469+ tiles++
470+ }
471+ }
466 // Inset sides with no neighbor (floats) light fully.472 // Inset sides with no neighbor (floats) light fully.
467 for _, strip := range borderStrips(f.Rect, f.Inset&^f.Neighbors, 0, s.Border.Width) {473 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))474 add(physRect(strip.X, strip.Y, strip.W, strip.H), parseColor(s.Border.Active))
@@ -475,15 +481,11 @@ func (r *Renderer) draws(s ports.Scene, contents map[ports.WindowID]ports.Surfac
475 if f.Inset&side != 0 {481 if f.Inset&side != 0 {
476 out = 0482 out = 0
477 }483 }
478- line := borderStrips(f.Rect, side, out, s.Border.Width)[0]484+ line := closeCorners(borderStrips(f.Rect, side, out, s.Border.Width)[0], *f, side, s.Border.Width)
479- for _, n := range s.Windows {485+ if tiles == 2 {
480- if n.ID == f.ID || n.Hidden || n.Popup || n.Neighbors&opposite(side) == 0 {486+ line = halfStrip(line, side)
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))
485- }
486 }487 }
488+ add(physRect(line.X, line.Y, line.W, line.H), parseColor(s.Border.Active))
487 }489 }
488 }490 }
489 // Popups draw only what the client drew, shadows clipped. Window popups491 // Popups draw only what the client drew, shadows clipped. Window popups
@@ -641,66 +643,40 @@ func borderStrips(r ports.Rect, sides ports.Sides, out, b int) []ports.Rect {
641 return strips643 return strips
642 }644 }
643 645
644-// opposite is the side facing side.646+// closeCorners runs a line along side of w b further at each end where
645-func opposite(side ports.Sides) ports.Sides {647+// the crossing line lies outside w (a neighbor's), so lit lines meet.
646- switch side {648+func closeCorners(line ports.Rect, w ports.SceneWindow, side ports.Sides, b int) ports.Rect {
647- case ports.SideLeft:
648- return ports.SideRight
649- case ports.SideRight:
650- return ports.SideLeft
651- case ports.SideTop:
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 }649 outside := func(s ports.Sides) bool { return w.Neighbors&s != 0 && w.Inset&s == 0 }
666- var lo, hi int650+ if side == ports.SideLeft || side == ports.SideRight {
667- switch side {651+ if outside(ports.SideTop) {
668- case ports.SideRight, ports.SideLeft:652+ line.Y, line.H = line.Y-b, line.H+b
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 }653 }
672- lo, hi = max(f.Y, n.Y), min(f.Y+f.H, n.Y+n.H)654+ if outside(ports.SideBottom) {
673- default:655+ line.H += b
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 }656 }
677- lo, hi = max(f.X, n.X), min(f.X+f.W, n.X+n.W)657+ return line
678 }658 }
679- if hi <= lo {659+ if outside(ports.SideLeft) {
680- return ports.Rect{}, false660+ line.X, line.W = line.X-b, line.W+b
681 }661 }
682- mid := lo + (hi-lo)/2662+ if outside(ports.SideRight) {
683- if side == ports.SideLeft || side == ports.SideTop {663+ line.W += b
684- lo = mid
685- } else {
686- hi = mid
687 }664 }
688- if side == ports.SideRight || side == ports.SideLeft {665+ return line
689- if lo == f.Y && outside(ports.SideTop) {666+}
690- lo -= b667+
691- }668+// halfStrip keeps the half of a separator strip owned by the window on
692- if hi == f.Y+f.H && outside(ports.SideBottom) {669+// side of it: the first half for the left or top window, else the second.
693- hi += b670+func halfStrip(r ports.Rect, side ports.Sides) ports.Rect {
694- }671+ switch side {
695- line.Y, line.H = lo, hi-lo672+ case ports.SideRight:
696- } else {673+ r.H /= 2
697- if lo == f.X && outside(ports.SideLeft) {674+ case ports.SideLeft:
698- lo -= b675+ r.Y, r.H = r.Y+r.H/2, r.H-r.H/2
699- }676+ case ports.SideBottom:
700- if hi == f.X+f.W && outside(ports.SideRight) {677+ r.W /= 2
701- hi += b678+ case ports.SideTop:
702- }679+ r.X, r.W = r.X+r.W/2, r.W-r.W/2
703- line.X, line.W = lo, hi-lo
704 }680 }
705- return line, true681+ return r
706 }682 }
M internal/adapters/vulkan/renderer_test.go +8 -10
@@ -77,29 +77,27 @@ 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.
80+ // Three columns (tmux rule): the focused window lights its full lines.
8281 three := s
8382 three.Windows = []ports.SceneWindow{
8483 {ID: 1, Rect: ports.Rect{X: 0, Y: 0, W: 16, H: 16}, Inset: ports.SideRight, Neighbors: ports.SideRight},
8584 {ID: 2, Rect: ports.Rect{X: 16, Y: 0, W: 16, H: 16}, Inset: ports.SideRight, Neighbors: ports.SideLeft | ports.SideRight, Focused: true},
8685 {ID: 3, Rect: ports.Rect{X: 32, Y: 0, W: 16, H: 16}, Neighbors: ports.SideLeft},
8786 }
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.
87+ check(three, map[image.Point]color.RGBA{{14, 3}: lit, {14, 12}: lit, {30, 3}: lit, {30, 12}: lit})
88+ three.Windows[1].Focused, three.Windows[0].Focused = false, true
89+ check(three, map[image.Point]color.RGBA{{14, 3}: lit, {14, 12}: lit, {30, 3}: gray, {30, 12}: gray})
90+ // Bottom-right tile of a split column: its left line and top line are
91+ // lit, and they meet in the corner.
9292 grid := s
9393 grid.Windows = []ports.SceneWindow{
9494 {ID: 1, Rect: ports.Rect{X: 0, Y: 0, W: 16, H: 32}, Inset: ports.SideRight, Neighbors: ports.SideRight},
9595 {ID: 2, Rect: ports.Rect{X: 16, Y: 0, W: 16, H: 16}, Inset: ports.SideBottom, Neighbors: ports.SideLeft | ports.SideBottom},
9696 {ID: 3, Rect: ports.Rect{X: 16, Y: 16, W: 16, H: 16}, Neighbors: ports.SideLeft | ports.SideTop, Focused: true},
9797 }
98- check(grid, map[image.Point]color.RGBA{{14, 30}: lit, {14, 20}: gray, {24, 14}: lit, {17, 14}: gray})
98+ check(grid, map[image.Point]color.RGBA{{14, 30}: lit, {14, 20}: lit, {14, 14}: lit, {14, 5}: gray, {24, 14}: lit, {17, 14}: lit})
9999 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})
100+ check(grid, map[image.Point]color.RGBA{{14, 3}: lit, {14, 14}: lit, {14, 15}: lit, {14, 20}: gray, {30, 14}: lit})
103101 s.Windows[0].Fullscreen = true
104102 check(s, map[image.Point]color.RGBA{{9, 9}: wc})
105103 s.Windows[0].Fullscreen = false
@@ -77,29 +77,27 @@ 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 middle80+ // Three columns (tmux rule): the focused window lights its full lines.
81- // lights the bottom half of the left line and the top half of the right one.
82 three := s81 three := s
83 three.Windows = []ports.SceneWindow{82 three.Windows = []ports.SceneWindow{
84 {ID: 1, Rect: ports.Rect{X: 0, Y: 0, W: 16, H: 16}, Inset: ports.SideRight, Neighbors: ports.SideRight},83 {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},84 {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},85 {ID: 3, Rect: ports.Rect{X: 32, Y: 0, W: 16, H: 16}, Neighbors: ports.SideLeft},
87 }86 }
88- check(three, map[image.Point]color.RGBA{{14, 3}: gray, {14, 12}: lit, {30, 3}: lit, {30, 12}: gray})87+ check(three, map[image.Point]color.RGBA{{14, 3}: lit, {14, 12}: lit, {30, 3}: lit, {30, 12}: lit})
89- // Bottom-right tile of a split column: its top line (owned by the tile88+ three.Windows[1].Focused, three.Windows[0].Focused = false, true
90- // above) meets the left line (owned by the left column); lit halves89+ check(three, map[image.Point]color.RGBA{{14, 3}: lit, {14, 12}: lit, {30, 3}: gray, {30, 12}: gray})
91- // ending at that corner close it.90+ // Bottom-right tile of a split column: its left line and top line are
91+ // lit, and they meet in the corner.
92 grid := s92 grid := s
93 grid.Windows = []ports.SceneWindow{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},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},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},96 {ID: 3, Rect: ports.Rect{X: 16, Y: 16, W: 16, H: 16}, Neighbors: ports.SideLeft | ports.SideTop, Focused: true},
97 }97 }
98- check(grid, map[image.Point]color.RGBA{{14, 30}: lit, {14, 20}: gray, {24, 14}: lit, {17, 14}: gray})98+ check(grid, map[image.Point]color.RGBA{{14, 30}: lit, {14, 20}: lit, {14, 14}: lit, {14, 5}: gray, {24, 14}: lit, {17, 14}: lit})
99 grid.Windows[2].Focused, grid.Windows[1].Focused = false, true99 grid.Windows[2].Focused, grid.Windows[1].Focused = false, true
100- // Top-right tile: left line bottom half and bottom line left half meet100+ check(grid, map[image.Point]color.RGBA{{14, 3}: lit, {14, 14}: lit, {14, 15}: lit, {14, 20}: gray, {30, 14}: lit})
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})
103 s.Windows[0].Fullscreen = true101 s.Windows[0].Fullscreen = true
104 check(s, map[image.Point]color.RGBA{{9, 9}: wc})102 check(s, map[image.Point]color.RGBA{{9, 9}: wc})
105 s.Windows[0].Fullscreen = false103 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 26

Tags 10

↑↓ moveenter openesc close