detached 82d6128

refactor(overview): place previews in policy-oriented slots

commit
82d612827e9782933ed033bb88bdfbece2040922
author
brice · · 2026-10-07 05:34 UTC
committer
brice · · 2026-10-07 05:42 UTC
parent
888535e
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +79 -50
M internal/core/overview.go +60 -37
@@ -363,10 +363,16 @@ func (m *Monitor) showOverview(w *Workspace) {
363363 m.selectRow()
364364 }
365365
366-// OverviewMove shares navigation rules across keys, scrolls and swipes.
367-// Vertical moves stay in a selected stash pile; left stays, and right
368-// deselects it, even when the row has no main card.
366+// OverviewMove shares navigation rules across keys, scrolls and swipes. It
367+// is the physical entry point: it maps dx and dy to the overview's moves.
369368 func (m *Monitor) OverviewMove(dx, dy int) {
369+ m.overviewMoveVertical(dx, dy)
370+}
371+
372+// overviewMoveVertical is the move of an overview whose workspaces stack
373+// vertically. Vertical moves stay in a selected stash pile; left stays, and
374+// right deselects it, even when the row has no main card.
375+func (m *Monitor) overviewMoveVertical(dx, dy int) {
370376 if dy != 0 {
371377 m.overviewVertical(dy)
372378 } else if dx != 0 {
@@ -383,8 +389,14 @@ func (m *Monitor) overviewVertical(dy int) {
383389 if m.moveStack(-dy) {
384390 return
385391 }
392+ m.overviewStep(dy)
393+}
394+
395+// overviewStep shows the workspace d rows from the current one; it stays
396+// on the first or last row.
397+func (m *Monitor) overviewStep(d int) {
386398 rows := m.overviewWorkspaces()
387- i := indexOf(rows, w) + dy
399+ i := indexOf(rows, m.Current()) + d
388400 if i >= 0 && i < len(rows) {
389401 m.showOverview(rows[i])
390402 }
@@ -487,16 +499,22 @@ func (m *Monitor) OverviewPick(id WindowID) {
487499 m.closeOverview()
488500 }
489501
502+// overviewAxis is the axis the overview lays workspaces out along, and the
503+// stacked cards of a row fan along.
504+func (m *Monitor) overviewAxis() layoutAxis { return verticalAxis }
505+
490506 // overviewRows centers the selected row with its neighbors above and below.
491-// The numbered/named boundary gets extra spacing and an inactive horizontal
492-// rule. Row widths, stash piles and preview scales remain unchanged.
493-func (m *Monitor) overviewRows() (map[*Workspace]int, []ports.Separator) {
507+// Each shown workspace gets a slot, in output-local pixels, to place its
508+// previews in. The numbered/named boundary gets extra spacing and an
509+// inactive horizontal rule. Row widths, stash piles and preview scales
510+// remain unchanged.
511+func (m *Monitor) overviewRows() (map[*Workspace]Rect, []ports.Separator) {
494512 cur := m.Current()
495513 u := cur.overviewArea()
496514 gap := max(1, u.H*3/100)
497515 hc := cur.rowHeight()
498516 y := u.Y + (u.H-hc)/2
499- rows := map[*Workspace]int{cur: y}
517+ rows := map[*Workspace]Rect{cur: {X: u.X, Y: y, W: u.W, H: hc}}
500518 var dividers []ports.Separator
501519 list := m.overviewWorkspaces()
502520 i := indexOf(list, cur)
@@ -510,12 +528,12 @@ func (m *Monitor) overviewRows() (map[*Workspace]int, []ports.Separator) {
510528 if boundary {
511529 spacing *= 2
512530 }
513- top := y + hc
531+ top, h := y+hc, w.rowHeight()
514532 if next < i {
515- rows[w] = y - spacing - w.rowHeight()
533+ rows[w] = Rect{X: u.X, Y: y - spacing - h, W: u.W, H: h}
516534 top = y - spacing
517535 } else {
518- rows[w] = y + hc + spacing
536+ rows[w] = Rect{X: u.X, Y: y + hc + spacing, W: u.W, H: h}
519537 }
520538 if boundary {
521539 margin := u.W * 4 / 100
@@ -531,12 +549,15 @@ func (m *Monitor) overviewRows() (map[*Workspace]int, []ports.Separator) {
531549 func (m *Monitor) overviewLayout(result []Placement) []Placement {
532550 cur := m.Current()
533551 rows, _ := m.overviewRows()
552+ axis := m.overviewAxis()
534553 for w := range m.all() {
535- ry, shown := rows[w]
554+ slot, shown := rows[w]
536555 if shown {
537556 at := m.cardAt(w)
538- result = append(result, m.stackRow(w, ry, w != cur, at < 0)...)
539- result = append(result, w.pile(ry, w != cur, at)...)
557+ result = append(result, m.stackRow(w, axis, slot, w != cur, at < 0)...)
558+ if len(w.Stash) > 0 {
559+ result = append(result, w.pile(w.frontSlot(axis, slot), w != cur, at)...)
560+ }
540561 }
541562 for _, f := range w.Floats {
542563 if _, drawn := w.itemOf(f.ID); !shown || !drawn {
@@ -637,11 +658,11 @@ func (w *Workspace) cardStep() int {
637658 return min(overviewCardStep, a.W/100, a.H/50)
638659 }
639660
640-// pile places the stash of w as cards on the left of the row whose top is
641-// at y: entry front (the stash selection when -1) in front, the next ones
642-// behind it, dimmed, the rest hidden. Only a front card of the current row
643-// (dim false) with front set is selected.
644-func (w *Workspace) pile(y int, dim bool, front int) []Placement {
661+// pile places the stash of w as cards on the left of band, the front band
662+// of its row (see frontSlot): entry front (the stash selection when -1) in
663+// front, the next ones behind it, dimmed, the rest hidden. Only a front card
664+// of the current row (dim false) with front set is selected.
665+func (w *Workspace) pile(band Rect, dim bool, front int) []Placement {
645666 n := len(w.Stash)
646667 if n == 0 {
647668 return nil
@@ -658,7 +679,7 @@ func (w *Workspace) pile(y int, dim bool, front int) []Placement {
658679 rowH := int(math.Round(float64(w.Usable.H) * w.overviewZoom()))
659680 // Stack rows reserve two peek steps above the front card. Align the
660681 // stash with that front band, not with the top of its envelope.
661- x0, y0 := u.X+u.W/100, w.frontRowY(y)+(rowH-ch)/2
682+ x0, y0 := band.X+u.W/100, band.Y+(rowH-ch)/2
662683 cards := make([][]Placement, n)
663684 for i := range w.Stash {
664685 cards[i] = []Placement{{ID: w.Stash[i].ID, Rect: Rect{X: x0, Y: y0, W: cw, H: ch}, Preview: overviewCardZoom, Focused: selected}}
@@ -692,13 +713,14 @@ func (w *Workspace) peekStep() int {
692713 return max(1, int(math.Round(float64(w.Usable.H)*w.overviewZoom()*overviewPeekStep)))
693714 }
694715
695-// frontRowY skips the space reserved for two cards peeking above the front.
696-// A single-card row starts at y, as before.
697-func (w *Workspace) frontRowY(y int) int {
716+// frontSlot is the part of slot the front card of w takes: slot shifted
717+// along axis past the room reserved for two cards peeking before it. A
718+// single-card row keeps slot.
719+func (w *Workspace) frontSlot(axis layoutAxis, slot Rect) Rect {
698720 if len(w.stack()) > 1 {
699- return y + 2*w.peekStep()
721+ axis.offset(&slot, 2*w.peekStep())
700722 }
701- return y
723+ return slot
702724 }
703725
704726 // rowHeight includes the largest possible stack envelope (two peeks above
@@ -782,29 +804,30 @@ func (w *Workspace) previewTilesInto(tiles []Placement) ([]Placement, int, Rect)
782804 return tiles, span, sel
783805 }
784806
785-// previewRow scales the tiles of w into a row whose top is at y, centred
786-// in the usable width, or scrolled so the focused column shows when the
807+// previewRow scales the tiles of w into a row placed in slot, centred
808+// in the slot's width, or scrolled so the focused column shows when the
787809 // row is wider, right of the stash pile. Dimmed rows (neighbor
788810 // workspaces) have no focus; neither has a row whose pile has it (lit
789811 // false).
790-func (w *Workspace) previewRow(y int, dim, lit bool) []Placement {
812+func (w *Workspace) previewRow(slot Rect, dim, lit bool) []Placement {
791813 tiles, span, sel := w.previewTiles()
792- return w.previewRowTiles(y, dim, lit, tiles, span, sel)
814+ return w.previewRowTiles(slot, dim, lit, tiles, span, sel)
793815 }
794816
795-func (w *Workspace) previewRowTiles(y int, dim, lit bool, tiles []Placement, span int, sel Rect) []Placement {
796- u := w.overviewArea()
817+// previewRowTiles scales tiles, laid out unscrolled over span with sel the
818+// focused one, into slot. Placement stays inside slot.
819+func (w *Workspace) previewRowTiles(slot Rect, dim, lit bool, tiles []Placement, span int, sel Rect) []Placement {
797820 z := w.overviewZoom()
798821 scale := func(v int) int { return int(math.Round(float64(v) * z)) }
799822 width := scale(span)
800823 // Rows stay centred on the output like rows without a stash; the pile
801824 // only pushes a row right when they would overlap.
802- x := max(u.X+(u.W-width)/2, u.X+w.pileWidth())
803825 left := w.pileWidth()
804- u.X, u.W = u.X+left, u.W-left
805- if width > u.W {
806- x = u.X + u.W/2 - scale(sel.X+sel.W/2)
807- x = min(max(x, u.X+u.W-width), u.X)
826+ x := max(slot.X+(slot.W-width)/2, slot.X+left)
827+ ux, uw := slot.X+left, slot.W-left
828+ if width > uw {
829+ x = ux + uw/2 - scale(sel.X+sel.W/2)
830+ x = min(max(x, ux+uw-width), ux)
808831 }
809832 for i := range tiles {
810833 if tiles[i].Hidden {
@@ -812,7 +835,7 @@ func (w *Workspace) previewRowTiles(y int, dim, lit bool, tiles []Placement, spa
812835 }
813836 r := tiles[i].Rect
814837 x0, y0 := scale(r.X), scale(r.Y)
815- tiles[i].Rect = Rect{X: x + x0, Y: y + y0, W: scale(r.X+r.W) - x0, H: scale(r.Y+r.H) - y0}
838+ tiles[i].Rect = Rect{X: x + x0, Y: slot.Y + y0, W: scale(r.X+r.W) - x0, H: scale(r.Y+r.H) - y0}
816839 tiles[i].Preview = z
817840 if dim {
818841 tiles[i].peeking(1)
@@ -363,10 +363,16 @@ func (m *Monitor) showOverview(w *Workspace) {
363 m.selectRow()363 m.selectRow()
364 }364 }
365 365
366-// OverviewMove shares navigation rules across keys, scrolls and swipes.366+// OverviewMove shares navigation rules across keys, scrolls and swipes. It
367-// Vertical moves stay in a selected stash pile; left stays, and right367+// is the physical entry point: it maps dx and dy to the overview's moves.
368-// deselects it, even when the row has no main card.
369 func (m *Monitor) OverviewMove(dx, dy int) {368 func (m *Monitor) OverviewMove(dx, dy int) {
369+ m.overviewMoveVertical(dx, dy)
370+}
371+
372+// overviewMoveVertical is the move of an overview whose workspaces stack
373+// vertically. Vertical moves stay in a selected stash pile; left stays, and
374+// right deselects it, even when the row has no main card.
375+func (m *Monitor) overviewMoveVertical(dx, dy int) {
370 if dy != 0 {376 if dy != 0 {
371 m.overviewVertical(dy)377 m.overviewVertical(dy)
372 } else if dx != 0 {378 } else if dx != 0 {
@@ -383,8 +389,14 @@ func (m *Monitor) overviewVertical(dy int) {
383 if m.moveStack(-dy) {389 if m.moveStack(-dy) {
384 return390 return
385 }391 }
392+ m.overviewStep(dy)
393+}
394+
395+// overviewStep shows the workspace d rows from the current one; it stays
396+// on the first or last row.
397+func (m *Monitor) overviewStep(d int) {
386 rows := m.overviewWorkspaces()398 rows := m.overviewWorkspaces()
387- i := indexOf(rows, w) + dy399+ i := indexOf(rows, m.Current()) + d
388 if i >= 0 && i < len(rows) {400 if i >= 0 && i < len(rows) {
389 m.showOverview(rows[i])401 m.showOverview(rows[i])
390 }402 }
@@ -487,16 +499,22 @@ func (m *Monitor) OverviewPick(id WindowID) {
487 m.closeOverview()499 m.closeOverview()
488 }500 }
489 501
502+// overviewAxis is the axis the overview lays workspaces out along, and the
503+// stacked cards of a row fan along.
504+func (m *Monitor) overviewAxis() layoutAxis { return verticalAxis }
505+
490 // overviewRows centers the selected row with its neighbors above and below.506 // overviewRows centers the selected row with its neighbors above and below.
491-// The numbered/named boundary gets extra spacing and an inactive horizontal507+// Each shown workspace gets a slot, in output-local pixels, to place its
492-// rule. Row widths, stash piles and preview scales remain unchanged.508+// previews in. The numbered/named boundary gets extra spacing and an
493-func (m *Monitor) overviewRows() (map[*Workspace]int, []ports.Separator) {509+// inactive horizontal rule. Row widths, stash piles and preview scales
510+// remain unchanged.
511+func (m *Monitor) overviewRows() (map[*Workspace]Rect, []ports.Separator) {
494 cur := m.Current()512 cur := m.Current()
495 u := cur.overviewArea()513 u := cur.overviewArea()
496 gap := max(1, u.H*3/100)514 gap := max(1, u.H*3/100)
497 hc := cur.rowHeight()515 hc := cur.rowHeight()
498 y := u.Y + (u.H-hc)/2516 y := u.Y + (u.H-hc)/2
499- rows := map[*Workspace]int{cur: y}517+ rows := map[*Workspace]Rect{cur: {X: u.X, Y: y, W: u.W, H: hc}}
500 var dividers []ports.Separator518 var dividers []ports.Separator
501 list := m.overviewWorkspaces()519 list := m.overviewWorkspaces()
502 i := indexOf(list, cur)520 i := indexOf(list, cur)
@@ -510,12 +528,12 @@ func (m *Monitor) overviewRows() (map[*Workspace]int, []ports.Separator) {
510 if boundary {528 if boundary {
511 spacing *= 2529 spacing *= 2
512 }530 }
513- top := y + hc531+ top, h := y+hc, w.rowHeight()
514 if next < i {532 if next < i {
515- rows[w] = y - spacing - w.rowHeight()533+ rows[w] = Rect{X: u.X, Y: y - spacing - h, W: u.W, H: h}
516 top = y - spacing534 top = y - spacing
517 } else {535 } else {
518- rows[w] = y + hc + spacing536+ rows[w] = Rect{X: u.X, Y: y + hc + spacing, W: u.W, H: h}
519 }537 }
520 if boundary {538 if boundary {
521 margin := u.W * 4 / 100539 margin := u.W * 4 / 100
@@ -531,12 +549,15 @@ func (m *Monitor) overviewRows() (map[*Workspace]int, []ports.Separator) {
531 func (m *Monitor) overviewLayout(result []Placement) []Placement {549 func (m *Monitor) overviewLayout(result []Placement) []Placement {
532 cur := m.Current()550 cur := m.Current()
533 rows, _ := m.overviewRows()551 rows, _ := m.overviewRows()
552+ axis := m.overviewAxis()
534 for w := range m.all() {553 for w := range m.all() {
535- ry, shown := rows[w]554+ slot, shown := rows[w]
536 if shown {555 if shown {
537 at := m.cardAt(w)556 at := m.cardAt(w)
538- result = append(result, m.stackRow(w, ry, w != cur, at < 0)...)557+ result = append(result, m.stackRow(w, axis, slot, w != cur, at < 0)...)
539- result = append(result, w.pile(ry, w != cur, at)...)558+ if len(w.Stash) > 0 {
559+ result = append(result, w.pile(w.frontSlot(axis, slot), w != cur, at)...)
560+ }
540 }561 }
541 for _, f := range w.Floats {562 for _, f := range w.Floats {
542 if _, drawn := w.itemOf(f.ID); !shown || !drawn {563 if _, drawn := w.itemOf(f.ID); !shown || !drawn {
@@ -637,11 +658,11 @@ func (w *Workspace) cardStep() int {
637 return min(overviewCardStep, a.W/100, a.H/50)658 return min(overviewCardStep, a.W/100, a.H/50)
638 }659 }
639 660
640-// pile places the stash of w as cards on the left of the row whose top is661+// pile places the stash of w as cards on the left of band, the front band
641-// at y: entry front (the stash selection when -1) in front, the next ones662+// of its row (see frontSlot): entry front (the stash selection when -1) in
642-// behind it, dimmed, the rest hidden. Only a front card of the current row663+// front, the next ones behind it, dimmed, the rest hidden. Only a front card
643-// (dim false) with front set is selected.664+// of the current row (dim false) with front set is selected.
644-func (w *Workspace) pile(y int, dim bool, front int) []Placement {665+func (w *Workspace) pile(band Rect, dim bool, front int) []Placement {
645 n := len(w.Stash)666 n := len(w.Stash)
646 if n == 0 {667 if n == 0 {
647 return nil668 return nil
@@ -658,7 +679,7 @@ func (w *Workspace) pile(y int, dim bool, front int) []Placement {
658 rowH := int(math.Round(float64(w.Usable.H) * w.overviewZoom()))679 rowH := int(math.Round(float64(w.Usable.H) * w.overviewZoom()))
659 // Stack rows reserve two peek steps above the front card. Align the680 // Stack rows reserve two peek steps above the front card. Align the
660 // stash with that front band, not with the top of its envelope.681 // stash with that front band, not with the top of its envelope.
661- x0, y0 := u.X+u.W/100, w.frontRowY(y)+(rowH-ch)/2682+ x0, y0 := band.X+u.W/100, band.Y+(rowH-ch)/2
662 cards := make([][]Placement, n)683 cards := make([][]Placement, n)
663 for i := range w.Stash {684 for i := range w.Stash {
664 cards[i] = []Placement{{ID: w.Stash[i].ID, Rect: Rect{X: x0, Y: y0, W: cw, H: ch}, Preview: overviewCardZoom, Focused: selected}}685 cards[i] = []Placement{{ID: w.Stash[i].ID, Rect: Rect{X: x0, Y: y0, W: cw, H: ch}, Preview: overviewCardZoom, Focused: selected}}
@@ -692,13 +713,14 @@ func (w *Workspace) peekStep() int {
692 return max(1, int(math.Round(float64(w.Usable.H)*w.overviewZoom()*overviewPeekStep)))713 return max(1, int(math.Round(float64(w.Usable.H)*w.overviewZoom()*overviewPeekStep)))
693 }714 }
694 715
695-// frontRowY skips the space reserved for two cards peeking above the front.716+// frontSlot is the part of slot the front card of w takes: slot shifted
696-// A single-card row starts at y, as before.717+// along axis past the room reserved for two cards peeking before it. A
697-func (w *Workspace) frontRowY(y int) int {718+// single-card row keeps slot.
719+func (w *Workspace) frontSlot(axis layoutAxis, slot Rect) Rect {
698 if len(w.stack()) > 1 {720 if len(w.stack()) > 1 {
699- return y + 2*w.peekStep()721+ axis.offset(&slot, 2*w.peekStep())
700 }722 }
701- return y723+ return slot
702 }724 }
703 725
704 // rowHeight includes the largest possible stack envelope (two peeks above726 // rowHeight includes the largest possible stack envelope (two peeks above
@@ -782,29 +804,30 @@ func (w *Workspace) previewTilesInto(tiles []Placement) ([]Placement, int, Rect)
782 return tiles, span, sel804 return tiles, span, sel
783 }805 }
784 806
785-// previewRow scales the tiles of w into a row whose top is at y, centred807+// previewRow scales the tiles of w into a row placed in slot, centred
786-// in the usable width, or scrolled so the focused column shows when the808+// in the slot's width, or scrolled so the focused column shows when the
787 // row is wider, right of the stash pile. Dimmed rows (neighbor809 // row is wider, right of the stash pile. Dimmed rows (neighbor
788 // workspaces) have no focus; neither has a row whose pile has it (lit810 // workspaces) have no focus; neither has a row whose pile has it (lit
789 // false).811 // false).
790-func (w *Workspace) previewRow(y int, dim, lit bool) []Placement {812+func (w *Workspace) previewRow(slot Rect, dim, lit bool) []Placement {
791 tiles, span, sel := w.previewTiles()813 tiles, span, sel := w.previewTiles()
792- return w.previewRowTiles(y, dim, lit, tiles, span, sel)814+ return w.previewRowTiles(slot, dim, lit, tiles, span, sel)
793 }815 }
794 816
795-func (w *Workspace) previewRowTiles(y int, dim, lit bool, tiles []Placement, span int, sel Rect) []Placement {817+// previewRowTiles scales tiles, laid out unscrolled over span with sel the
796- u := w.overviewArea()818+// focused one, into slot. Placement stays inside slot.
819+func (w *Workspace) previewRowTiles(slot Rect, dim, lit bool, tiles []Placement, span int, sel Rect) []Placement {
797 z := w.overviewZoom()820 z := w.overviewZoom()
798 scale := func(v int) int { return int(math.Round(float64(v) * z)) }821 scale := func(v int) int { return int(math.Round(float64(v) * z)) }
799 width := scale(span)822 width := scale(span)
800 // Rows stay centred on the output like rows without a stash; the pile823 // Rows stay centred on the output like rows without a stash; the pile
801 // only pushes a row right when they would overlap.824 // only pushes a row right when they would overlap.
802- x := max(u.X+(u.W-width)/2, u.X+w.pileWidth())
803 left := w.pileWidth()825 left := w.pileWidth()
804- u.X, u.W = u.X+left, u.W-left826+ x := max(slot.X+(slot.W-width)/2, slot.X+left)
805- if width > u.W {827+ ux, uw := slot.X+left, slot.W-left
806- x = u.X + u.W/2 - scale(sel.X+sel.W/2)828+ if width > uw {
807- x = min(max(x, u.X+u.W-width), u.X)829+ x = ux + uw/2 - scale(sel.X+sel.W/2)
830+ x = min(max(x, ux+uw-width), ux)
808 }831 }
809 for i := range tiles {832 for i := range tiles {
810 if tiles[i].Hidden {833 if tiles[i].Hidden {
@@ -812,7 +835,7 @@ func (w *Workspace) previewRowTiles(y int, dim, lit bool, tiles []Placement, spa
812 }835 }
813 r := tiles[i].Rect836 r := tiles[i].Rect
814 x0, y0 := scale(r.X), scale(r.Y)837 x0, y0 := scale(r.X), scale(r.Y)
815- tiles[i].Rect = Rect{X: x + x0, Y: y + y0, W: scale(r.X+r.W) - x0, H: scale(r.Y+r.H) - y0}838+ tiles[i].Rect = Rect{X: x + x0, Y: slot.Y + y0, W: scale(r.X+r.W) - x0, H: scale(r.Y+r.H) - y0}
816 tiles[i].Preview = z839 tiles[i].Preview = z
817 if dim {840 if dim {
818 tiles[i].peeking(1)841 tiles[i].peeking(1)
M internal/core/overview_stack.go +19 -13
@@ -234,10 +234,10 @@ func (w *Workspace) apply(item stackItem, selected WindowID) {
234234
235235 // previewItem draws a group using screen geometry and a single card at its
236236 // configured size. A prior maximized column retains its full-width buffer.
237-func (m *Monitor) previewItem(w *Workspace, item stackItem, y int, dim, lit bool) []Placement {
237+func (m *Monitor) previewItem(w *Workspace, item stackItem, slot Rect, dim, lit bool) []Placement {
238238 if item.kind == stackColumns {
239239 if !w.overviewMaximized() {
240- return w.previewRow(y, dim, lit)
240+ return w.previewRow(slot, dim, lit)
241241 }
242242 // The hidden columns fill the screen as if the maximized column,
243243 // shown on its own card, were not there. In particular, an earlier
@@ -260,20 +260,20 @@ func (m *Monitor) previewItem(w *Workspace, item stackItem, y int, dim, lit bool
260260 }
261261 sel := rects[selected]
262262 sel.X -= w.Usable.X
263- return w.previewRowTiles(y, dim, lit, tiles, w.Usable.W, sel)
263+ return w.previewRowTiles(slot, dim, lit, tiles, w.Usable.W, sel)
264264 }
265265 g := w.gap()
266266 if item.kind == stackFloat && item.id == w.cover() {
267267 // A fullscreen float shows at the size of a fullscreen column.
268268 r := Rect{X: g, Y: g, W: max(w.Usable.W-2*g, 0), H: max(w.Usable.H-2*g, 0)}
269- return w.previewRowTiles(y, dim, lit, []Placement{{ID: item.id, Rect: r, Focused: true, Fullscreen: true}}, w.Usable.W, r)
269+ return w.previewRowTiles(slot, dim, lit, []Placement{{ID: item.id, Rect: r, Focused: true, Fullscreen: true}}, w.Usable.W, r)
270270 }
271271 if item.kind == stackFloat {
272272 f := w.Floats[w.floatIndex(item.id)]
273273 r := w.floatRect(f)
274274 r.X -= w.Usable.X
275275 r.Y -= w.Usable.Y
276- return w.previewRowTiles(y, dim, lit, []Placement{{ID: item.id, Rect: r, Floating: true, Focused: true}}, w.Usable.W, r)
276+ return w.previewRowTiles(slot, dim, lit, []Placement{{ID: item.id, Rect: r, Floating: true, Focused: true}}, w.Usable.W, r)
277277 }
278278 i := w.columnOf(item.id)
279279 if i < 0 {
@@ -288,7 +288,7 @@ func (m *Monitor) previewItem(w *Workspace, item stackItem, y int, dim, lit bool
288288 for j, t := range w.rowBuf {
289289 tiles = append(tiles, Placement{ID: c.Windows[j], Rect: t, Focused: j == c.Focus, Fullscreen: c.Windows[j] == w.cover()})
290290 }
291- return w.previewRowTiles(y, dim, lit, tiles, w.Usable.W, r)
291+ return w.previewRowTiles(slot, dim, lit, tiles, w.Usable.W, r)
292292 }
293293
294294 // hiddenColumnRects lays out the columns behind the maximized one with the
@@ -301,14 +301,15 @@ func (w *Workspace) hiddenColumnRects() []Rect {
301301 return slices.Insert(v.columnRectsFor(true), w.Focus, Rect{})
302302 }
303303
304-// stackRow draws up to two cards on each side of the provisional front.
305-func (m *Monitor) stackRow(w *Workspace, y int, dim, lit bool) []Placement {
304+// stackRow draws up to two cards on each side of the provisional front,
305+// fanned along axis.
306+func (m *Monitor) stackRow(w *Workspace, axis layoutAxis, slot Rect, dim, lit bool) []Placement {
306307 items := w.stack()
307308 if len(items) == 0 {
308- return w.previewRow(y, dim, lit)
309+ return w.previewRow(slot, dim, lit)
309310 }
310311 if len(items) == 1 {
311- return m.previewItem(w, items[0], y, dim, lit)
312+ return m.previewItem(w, items[0], slot, dim, lit)
312313 }
313314 at := 0
314315 if w == m.Current() {
@@ -316,12 +317,17 @@ func (m *Monitor) stackRow(w *Workspace, y int, dim, lit bool) []Placement {
316317 }
317318 cards := make([][]Placement, len(items))
318319 step := w.peekStep()
320+ // along is the offset of d steps on the overview axis.
321+ along := func(d int) (int, int) {
322+ var r Rect
323+ axis.offset(&r, d*step)
324+ return r.X, r.Y
325+ }
319326 for i, item := range items {
320- cards[i] = m.previewItem(w, item, w.frontRowY(y), dim, lit)
327+ cards[i] = m.previewItem(w, item, w.frontSlot(axis, slot), dim, lit)
321328 }
322329
323330 // fan's behind side advances through the linear stack.
324331 return fan(cards, at, 2, 2, false,
325- func(d int) (int, int) { return 0, -d * step },
326- func(d int) (int, int) { return 0, d * step }, dim, lit)
332+ func(d int) (int, int) { return along(-d) }, along, dim, lit)
327333 }
@@ -234,10 +234,10 @@ func (w *Workspace) apply(item stackItem, selected WindowID) {
234 234
235 // previewItem draws a group using screen geometry and a single card at its235 // previewItem draws a group using screen geometry and a single card at its
236 // configured size. A prior maximized column retains its full-width buffer.236 // configured size. A prior maximized column retains its full-width buffer.
237-func (m *Monitor) previewItem(w *Workspace, item stackItem, y int, dim, lit bool) []Placement {237+func (m *Monitor) previewItem(w *Workspace, item stackItem, slot Rect, dim, lit bool) []Placement {
238 if item.kind == stackColumns {238 if item.kind == stackColumns {
239 if !w.overviewMaximized() {239 if !w.overviewMaximized() {
240- return w.previewRow(y, dim, lit)240+ return w.previewRow(slot, dim, lit)
241 }241 }
242 // The hidden columns fill the screen as if the maximized column,242 // The hidden columns fill the screen as if the maximized column,
243 // shown on its own card, were not there. In particular, an earlier243 // shown on its own card, were not there. In particular, an earlier
@@ -260,20 +260,20 @@ func (m *Monitor) previewItem(w *Workspace, item stackItem, y int, dim, lit bool
260 }260 }
261 sel := rects[selected]261 sel := rects[selected]
262 sel.X -= w.Usable.X262 sel.X -= w.Usable.X
263- return w.previewRowTiles(y, dim, lit, tiles, w.Usable.W, sel)263+ return w.previewRowTiles(slot, dim, lit, tiles, w.Usable.W, sel)
264 }264 }
265 g := w.gap()265 g := w.gap()
266 if item.kind == stackFloat && item.id == w.cover() {266 if item.kind == stackFloat && item.id == w.cover() {
267 // A fullscreen float shows at the size of a fullscreen column.267 // A fullscreen float shows at the size of a fullscreen column.
268 r := Rect{X: g, Y: g, W: max(w.Usable.W-2*g, 0), H: max(w.Usable.H-2*g, 0)}268 r := Rect{X: g, Y: g, W: max(w.Usable.W-2*g, 0), H: max(w.Usable.H-2*g, 0)}
269- return w.previewRowTiles(y, dim, lit, []Placement{{ID: item.id, Rect: r, Focused: true, Fullscreen: true}}, w.Usable.W, r)269+ return w.previewRowTiles(slot, dim, lit, []Placement{{ID: item.id, Rect: r, Focused: true, Fullscreen: true}}, w.Usable.W, r)
270 }270 }
271 if item.kind == stackFloat {271 if item.kind == stackFloat {
272 f := w.Floats[w.floatIndex(item.id)]272 f := w.Floats[w.floatIndex(item.id)]
273 r := w.floatRect(f)273 r := w.floatRect(f)
274 r.X -= w.Usable.X274 r.X -= w.Usable.X
275 r.Y -= w.Usable.Y275 r.Y -= w.Usable.Y
276- return w.previewRowTiles(y, dim, lit, []Placement{{ID: item.id, Rect: r, Floating: true, Focused: true}}, w.Usable.W, r)276+ return w.previewRowTiles(slot, dim, lit, []Placement{{ID: item.id, Rect: r, Floating: true, Focused: true}}, w.Usable.W, r)
277 }277 }
278 i := w.columnOf(item.id)278 i := w.columnOf(item.id)
279 if i < 0 {279 if i < 0 {
@@ -288,7 +288,7 @@ func (m *Monitor) previewItem(w *Workspace, item stackItem, y int, dim, lit bool
288 for j, t := range w.rowBuf {288 for j, t := range w.rowBuf {
289 tiles = append(tiles, Placement{ID: c.Windows[j], Rect: t, Focused: j == c.Focus, Fullscreen: c.Windows[j] == w.cover()})289 tiles = append(tiles, Placement{ID: c.Windows[j], Rect: t, Focused: j == c.Focus, Fullscreen: c.Windows[j] == w.cover()})
290 }290 }
291- return w.previewRowTiles(y, dim, lit, tiles, w.Usable.W, r)291+ return w.previewRowTiles(slot, dim, lit, tiles, w.Usable.W, r)
292 }292 }
293 293
294 // hiddenColumnRects lays out the columns behind the maximized one with the294 // hiddenColumnRects lays out the columns behind the maximized one with the
@@ -301,14 +301,15 @@ func (w *Workspace) hiddenColumnRects() []Rect {
301 return slices.Insert(v.columnRectsFor(true), w.Focus, Rect{})301 return slices.Insert(v.columnRectsFor(true), w.Focus, Rect{})
302 }302 }
303 303
304-// stackRow draws up to two cards on each side of the provisional front.304+// stackRow draws up to two cards on each side of the provisional front,
305-func (m *Monitor) stackRow(w *Workspace, y int, dim, lit bool) []Placement {305+// fanned along axis.
306+func (m *Monitor) stackRow(w *Workspace, axis layoutAxis, slot Rect, dim, lit bool) []Placement {
306 items := w.stack()307 items := w.stack()
307 if len(items) == 0 {308 if len(items) == 0 {
308- return w.previewRow(y, dim, lit)309+ return w.previewRow(slot, dim, lit)
309 }310 }
310 if len(items) == 1 {311 if len(items) == 1 {
311- return m.previewItem(w, items[0], y, dim, lit)312+ return m.previewItem(w, items[0], slot, dim, lit)
312 }313 }
313 at := 0314 at := 0
314 if w == m.Current() {315 if w == m.Current() {
@@ -316,12 +317,17 @@ func (m *Monitor) stackRow(w *Workspace, y int, dim, lit bool) []Placement {
316 }317 }
317 cards := make([][]Placement, len(items))318 cards := make([][]Placement, len(items))
318 step := w.peekStep()319 step := w.peekStep()
320+ // along is the offset of d steps on the overview axis.
321+ along := func(d int) (int, int) {
322+ var r Rect
323+ axis.offset(&r, d*step)
324+ return r.X, r.Y
325+ }
319 for i, item := range items {326 for i, item := range items {
320- cards[i] = m.previewItem(w, item, w.frontRowY(y), dim, lit)327+ cards[i] = m.previewItem(w, item, w.frontSlot(axis, slot), dim, lit)
321 }328 }
322 329
323 // fan's behind side advances through the linear stack.330 // fan's behind side advances through the linear stack.
324 return fan(cards, at, 2, 2, false,331 return fan(cards, at, 2, 2, false,
325- func(d int) (int, int) { return 0, -d * step },332+ func(d int) (int, int) { return along(-d) }, along, dim, lit)
326- func(d int) (int, int) { return 0, d * step }, dim, lit)
327 }333 }

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