detached 6ee8ac0

fix(overview): separate drawn neighbor envelopes and lock swipe axes

commit
6ee8ac0488b81f20f045e694cb7b51fcbda4f2db
author
brice · · 2026-10-06 14:32 UTC
parent
aee8623
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
1 file · +53 -6
M internal/core/overview.go +53 -6
@@ -490,9 +490,8 @@ func (m *Monitor) OverviewPick(id WindowID) {
490490 m.closeOverview()
491491 }
492492
493-// overviewRows centers the selected row with its neighbors above and below.
494-// The numbered/named boundary gets extra spacing and an inactive horizontal
495-// rule. Row widths, stash piles and preview scales remain unchanged.
493+// overviewRows places neighboring workspaces along the current policy axis.
494+// Named boundaries reserve extra spacing and a perpendicular divider.
496495 func (m *Monitor) overviewRows() (map[*Workspace]Rect, []ports.Separator) {
497496 cur := m.Current()
498497 u := cur.overviewArea()
@@ -552,6 +551,12 @@ func (m *Monitor) overviewRows() (map[*Workspace]Rect, []ports.Separator) {
552551 func (m *Monitor) overviewLayout(result []Placement) []Placement {
553552 cur := m.Current()
554553 rows, _ := m.overviewRows()
554+ type group struct {
555+ w *Workspace
556+ start, end int
557+ }
558+ var groups [3]group
559+ count := 0
555560 for w := range m.all() {
556561 ry, shown := rows[w]
557562 if shown {
@@ -559,6 +564,8 @@ func (m *Monitor) overviewLayout(result []Placement) []Placement {
559564 start := len(result)
560565 result = append(result, m.stackRow(w, ry.Y, w != cur, at < 0)...)
561566 result = append(result, w.pile(ry.Y, w != cur, at)...)
567+ groups[count] = group{w, start, len(result)}
568+ count++
562569 for i := start; i < len(result); i++ {
563570 if !result[i].Hidden {
564571 result[i].Rect.X += ry.X
@@ -575,6 +582,46 @@ func (m *Monitor) overviewLayout(result []Placement) []Placement {
575582 result = w.appendHidden(result, false)
576583 }
577584 }
585+ if cur.policy().workspace == horizontalAxis {
586+ left, right := cur.overviewArea().X+cur.overviewArea().W, cur.overviewArea().X
587+ for _, g := range groups[:count] {
588+ if g.w != cur {
589+ continue
590+ }
591+ for _, p := range result[g.start:g.end] {
592+ if !p.Hidden {
593+ left = min(left, p.Rect.X)
594+ right = max(right, p.Rect.X+p.Rect.W)
595+ }
596+ }
597+ }
598+ gap := max(1, cur.overviewArea().H*3/100)
599+ for _, g := range groups[:count] {
600+ w, origin := g.w, rows[g.w]
601+ if w == cur {
602+ continue
603+ }
604+ lo, hi := int(^uint(0)>>1), -int(^uint(0)>>1)
605+ for _, p := range result[g.start:g.end] {
606+ if !p.Hidden {
607+ lo = min(lo, p.Rect.X)
608+ hi = max(hi, p.Rect.X+p.Rect.W)
609+ }
610+ }
611+ if lo > hi {
612+ continue
613+ }
614+ dx := right + gap - lo
615+ if origin.X < 0 {
616+ dx = left - gap - hi
617+ }
618+ for i := g.start; i < g.end; i++ {
619+ if !result[i].Hidden {
620+ result[i].Rect.X += dx
621+ }
622+ }
623+ }
624+ }
578625 return result
579626 }
580627
@@ -757,15 +804,13 @@ func (w *Workspace) rowHeight() int {
757804 return h
758805 }
759806
760-// previewTiles lays tiles out unscrolled, relative to the usable area's
761-// corner. Fixed overflow uses its on-screen geometry; scroll overflow uses
762-// an unscrolled row. span is the row's width; sel is the focused column.
763807 // overviewPreview describes unscaled content independently of its presentation.
764808 type overviewPreview struct {
765809 bounds Rect
766810 selected Rect
767811 }
768812
813+// previewTiles lays out unscrolled tiles relative to the usable origin.
769814 func (w *Workspace) previewTiles() ([]Placement, overviewPreview) {
770815 return w.previewTilesInto(make([]Placement, 0, w.columnWindows()))
771816 }
@@ -1068,6 +1113,8 @@ func (m *Monitor) overviewScroll(a ports.PointerAxis) (changed bool) {
10681113 axis := m.Current().policy().workspace
10691114 m.OverviewMove(0, step)
10701115 if axis != m.Current().policy().workspace {
1116+ m.ov.scrollX, m.ov.scrollY = 0, 0
1117+ m.ov.scrolled = finger
10711118 return true
10721119 }
10731120 m.ov.scrollY -= float64(step) * overviewScrollStep
@@ -490,9 +490,8 @@ func (m *Monitor) OverviewPick(id WindowID) {
490 m.closeOverview()490 m.closeOverview()
491 }491 }
492 492
493-// overviewRows centers the selected row with its neighbors above and below.493+// overviewRows places neighboring workspaces along the current policy axis.
494-// The numbered/named boundary gets extra spacing and an inactive horizontal494+// Named boundaries reserve extra spacing and a perpendicular divider.
495-// rule. Row widths, stash piles and preview scales remain unchanged.
496 func (m *Monitor) overviewRows() (map[*Workspace]Rect, []ports.Separator) {495 func (m *Monitor) overviewRows() (map[*Workspace]Rect, []ports.Separator) {
497 cur := m.Current()496 cur := m.Current()
498 u := cur.overviewArea()497 u := cur.overviewArea()
@@ -552,6 +551,12 @@ func (m *Monitor) overviewRows() (map[*Workspace]Rect, []ports.Separator) {
552 func (m *Monitor) overviewLayout(result []Placement) []Placement {551 func (m *Monitor) overviewLayout(result []Placement) []Placement {
553 cur := m.Current()552 cur := m.Current()
554 rows, _ := m.overviewRows()553 rows, _ := m.overviewRows()
554+ type group struct {
555+ w *Workspace
556+ start, end int
557+ }
558+ var groups [3]group
559+ count := 0
555 for w := range m.all() {560 for w := range m.all() {
556 ry, shown := rows[w]561 ry, shown := rows[w]
557 if shown {562 if shown {
@@ -559,6 +564,8 @@ func (m *Monitor) overviewLayout(result []Placement) []Placement {
559 start := len(result)564 start := len(result)
560 result = append(result, m.stackRow(w, ry.Y, w != cur, at < 0)...)565 result = append(result, m.stackRow(w, ry.Y, w != cur, at < 0)...)
561 result = append(result, w.pile(ry.Y, w != cur, at)...)566 result = append(result, w.pile(ry.Y, w != cur, at)...)
567+ groups[count] = group{w, start, len(result)}
568+ count++
562 for i := start; i < len(result); i++ {569 for i := start; i < len(result); i++ {
563 if !result[i].Hidden {570 if !result[i].Hidden {
564 result[i].Rect.X += ry.X571 result[i].Rect.X += ry.X
@@ -575,6 +582,46 @@ func (m *Monitor) overviewLayout(result []Placement) []Placement {
575 result = w.appendHidden(result, false)582 result = w.appendHidden(result, false)
576 }583 }
577 }584 }
585+ if cur.policy().workspace == horizontalAxis {
586+ left, right := cur.overviewArea().X+cur.overviewArea().W, cur.overviewArea().X
587+ for _, g := range groups[:count] {
588+ if g.w != cur {
589+ continue
590+ }
591+ for _, p := range result[g.start:g.end] {
592+ if !p.Hidden {
593+ left = min(left, p.Rect.X)
594+ right = max(right, p.Rect.X+p.Rect.W)
595+ }
596+ }
597+ }
598+ gap := max(1, cur.overviewArea().H*3/100)
599+ for _, g := range groups[:count] {
600+ w, origin := g.w, rows[g.w]
601+ if w == cur {
602+ continue
603+ }
604+ lo, hi := int(^uint(0)>>1), -int(^uint(0)>>1)
605+ for _, p := range result[g.start:g.end] {
606+ if !p.Hidden {
607+ lo = min(lo, p.Rect.X)
608+ hi = max(hi, p.Rect.X+p.Rect.W)
609+ }
610+ }
611+ if lo > hi {
612+ continue
613+ }
614+ dx := right + gap - lo
615+ if origin.X < 0 {
616+ dx = left - gap - hi
617+ }
618+ for i := g.start; i < g.end; i++ {
619+ if !result[i].Hidden {
620+ result[i].Rect.X += dx
621+ }
622+ }
623+ }
624+ }
578 return result625 return result
579 }626 }
580 627
@@ -757,15 +804,13 @@ func (w *Workspace) rowHeight() int {
757 return h804 return h
758 }805 }
759 806
760-// previewTiles lays tiles out unscrolled, relative to the usable area's
761-// corner. Fixed overflow uses its on-screen geometry; scroll overflow uses
762-// an unscrolled row. span is the row's width; sel is the focused column.
763 // overviewPreview describes unscaled content independently of its presentation.807 // overviewPreview describes unscaled content independently of its presentation.
764 type overviewPreview struct {808 type overviewPreview struct {
765 bounds Rect809 bounds Rect
766 selected Rect810 selected Rect
767 }811 }
768 812
813+// previewTiles lays out unscrolled tiles relative to the usable origin.
769 func (w *Workspace) previewTiles() ([]Placement, overviewPreview) {814 func (w *Workspace) previewTiles() ([]Placement, overviewPreview) {
770 return w.previewTilesInto(make([]Placement, 0, w.columnWindows()))815 return w.previewTilesInto(make([]Placement, 0, w.columnWindows()))
771 }816 }
@@ -1068,6 +1113,8 @@ func (m *Monitor) overviewScroll(a ports.PointerAxis) (changed bool) {
1068 axis := m.Current().policy().workspace1113 axis := m.Current().policy().workspace
1069 m.OverviewMove(0, step)1114 m.OverviewMove(0, step)
1070 if axis != m.Current().policy().workspace {1115 if axis != m.Current().policy().workspace {
1116+ m.ov.scrollX, m.ov.scrollY = 0, 0
1117+ m.ov.scrolled = finger
1071 return true1118 return true
1072 }1119 }
1073 m.ov.scrollY -= float64(step) * overviewScrollStep1120 m.ov.scrollY -= float64(step) * overviewScrollStep

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