feat(core): track recently used columns
- commit
- 890c4374b9b3f2459596bf031e21c378e426c352
- author
- brice · · 2026-10-08 17:19 UTC
- parent
- 65119ef
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
5 files · +220 -2
M internal/core/core.go +4 -0
| @@ -596,6 +596,10 @@ func (c *Core) publishFrame(ctx context.Context, only *screen) error { | |||
| 596 | 596 | if !sc.mon.ov.open { | |
| 597 | 597 | sc.mon.Current().settleLeft() | |
| 598 | 598 | } | |
| 599 | + | // A switcher's selection is provisional too. | |
| 600 | + | if !sc.mon.previewing() { | |
| 601 | + | sc.mon.Current().noteFocus() | |
| 602 | + | } | |
| 599 | 603 | } | |
| 600 | 604 | c.refreshShown() | |
| 601 | 605 | c.captureExpire() | |
| @@ -596,6 +596,10 @@ func (c *Core) publishFrame(ctx context.Context, only *screen) error { | |||||
| 596 | if !sc.mon.ov.open { | 596 | if !sc.mon.ov.open { | ||
| 597 | sc.mon.Current().settleLeft() | 597 | sc.mon.Current().settleLeft() | ||
| 598 | } | 598 | } | ||
| 599 | + | // A switcher's selection is provisional too. | |||
| 600 | + | if !sc.mon.previewing() { | |||
| 601 | + | sc.mon.Current().noteFocus() | |||
| 602 | + | } | |||
| 599 | } | 603 | } | ||
| 600 | c.refreshShown() | 604 | c.refreshShown() | ||
| 601 | c.captureExpire() | 605 | c.captureExpire() | ||
M internal/core/layout.go +6 -2
| @@ -170,8 +170,11 @@ type Workspace struct { | |||
| 170 | 170 | // maximized remembers recently maximized columns by window ID, newest | |
| 171 | 171 | // first; stack() filters removed windows at read time. | |
| 172 | 172 | maximized []WindowID | |
| 173 | - | Focus int | |
| 174 | - | View int | |
| 173 | + | // recent holds one window per column the user focused, newest first | |
| 174 | + | // (switcher.go); columns that went are dropped when read. | |
| 175 | + | recent []WindowID | |
| 176 | + | Focus int | |
| 177 | + | View int | |
| 175 | 178 | // View is the settled offset along the layout's content axis. | |
| 176 | 179 | // view is its presentation offset, in logical pixels. | |
| 177 | 180 | view slide | |
| @@ -641,6 +644,7 @@ func (w *Workspace) unslot(n int) { | |||
| 641 | 644 | ||
| 642 | 645 | func (w *Workspace) RemoveWindow(id WindowID) { | |
| 643 | 646 | w.maximized = slices.DeleteFunc(w.maximized, func(v WindowID) bool { return v == id }) | |
| 647 | + | w.recent = slices.DeleteFunc(w.recent, func(v WindowID) bool { return v == id }) | |
| 644 | 648 | if w.fullscreen == id { | |
| 645 | 649 | w.fullscreen = 0 | |
| 646 | 650 | } | |
| @@ -170,8 +170,11 @@ type Workspace struct { | |||||
| 170 | // maximized remembers recently maximized columns by window ID, newest | 170 | // maximized remembers recently maximized columns by window ID, newest | ||
| 171 | // first; stack() filters removed windows at read time. | 171 | // first; stack() filters removed windows at read time. | ||
| 172 | maximized []WindowID | 172 | maximized []WindowID | ||
| 173 | - | Focus int | 173 | + | // recent holds one window per column the user focused, newest first |
| 174 | - | View int | 174 | + | // (switcher.go); columns that went are dropped when read. |
| 175 | + | recent []WindowID | |||
| 176 | + | Focus int | |||
| 177 | + | View int | |||
| 175 | // View is the settled offset along the layout's content axis. | 178 | // View is the settled offset along the layout's content axis. | ||
| 176 | // view is its presentation offset, in logical pixels. | 179 | // view is its presentation offset, in logical pixels. | ||
| 177 | view slide | 180 | view slide | ||
| @@ -641,6 +644,7 @@ func (w *Workspace) unslot(n int) { | |||||
| 641 | 644 | ||||
| 642 | func (w *Workspace) RemoveWindow(id WindowID) { | 645 | func (w *Workspace) RemoveWindow(id WindowID) { | ||
| 643 | w.maximized = slices.DeleteFunc(w.maximized, func(v WindowID) bool { return v == id }) | 646 | w.maximized = slices.DeleteFunc(w.maximized, func(v WindowID) bool { return v == id }) | ||
| 647 | + | w.recent = slices.DeleteFunc(w.recent, func(v WindowID) bool { return v == id }) | |||
| 644 | if w.fullscreen == id { | 648 | if w.fullscreen == id { | ||
| 645 | w.fullscreen = 0 | 649 | w.fullscreen = 0 | ||
| 646 | } | 650 | } | ||
M internal/core/monitor.go +2 -0
| @@ -45,6 +45,8 @@ type Monitor struct { | |||
| 45 | 45 | switchList []*Workspace | |
| 46 | 46 | // ov holds the overview selection and its Escape snapshot. | |
| 47 | 47 | ov overviewState | |
| 48 | + | // sw is the column switcher (switcher.go). | |
| 49 | + | sw switcherState | |
| 48 | 50 | // overviewOpens counts the overview's openings: a swipe sliding when | |
| 49 | 51 | // one happens is dropped (gesture.go). | |
| 50 | 52 | overviewOpens int | |
| @@ -45,6 +45,8 @@ type Monitor struct { | |||||
| 45 | switchList []*Workspace | 45 | switchList []*Workspace | ||
| 46 | // ov holds the overview selection and its Escape snapshot. | 46 | // ov holds the overview selection and its Escape snapshot. | ||
| 47 | ov overviewState | 47 | ov overviewState | ||
| 48 | + | // sw is the column switcher (switcher.go). | |||
| 49 | + | sw switcherState | |||
| 48 | // overviewOpens counts the overview's openings: a swipe sliding when | 50 | // overviewOpens counts the overview's openings: a swipe sliding when | ||
| 49 | // one happens is dropped (gesture.go). | 51 | // one happens is dropped (gesture.go). | ||
| 50 | overviewOpens int | 52 | overviewOpens int | ||
A internal/core/switcher.go +79 -0
| @@ -0,0 +1,79 @@ | |||
| 1 | + | package core | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "slices" | |
| 5 | + | ) | |
| 6 | + | ||
| 7 | + | // The column switcher is an Alt+Tab for the tiled columns of the workspace on | |
| 8 | + | // screen. Each workspace keeps the columns it focused, most recent first | |
| 9 | + | // (recent); the switcher steps through them without moving the focus, and | |
| 10 | + | // commits the selected one when the command key is released. | |
| 11 | + | ||
| 12 | + | // switcherRecentMax caps a workspace's recent list. | |
| 13 | + | const switcherRecentMax = 64 | |
| 14 | + | ||
| 15 | + | // switcherState is the switcher of a monitor. A window stands for each | |
| 16 | + | // column in order, the focused column first. Nothing moves until the commit; | |
| 17 | + | // shown is set once the cards are drawn. | |
| 18 | + | type switcherState struct { | |
| 19 | + | open, shown bool | |
| 20 | + | ws *Workspace | |
| 21 | + | order []WindowID | |
| 22 | + | at int | |
| 23 | + | // sizes are the client sizes the cards are scaled from. | |
| 24 | + | sizes map[WindowID]Rect | |
| 25 | + | } | |
| 26 | + | ||
| 27 | + | // previewing reports whether the monitor draws previews instead of its | |
| 28 | + | // workspace: the overview, or the switcher's cards. | |
| 29 | + | func (m *Monitor) previewing() bool { return m.ov.open || m.sw.shown } | |
| 30 | + | ||
| 31 | + | // noteFocus records the focused column as the most recently used. It does | |
| 32 | + | // nothing while a float or the stash has the focus, and allocates nothing | |
| 33 | + | // when the focused column is already the latest. | |
| 34 | + | func (w *Workspace) noteFocus() { | |
| 35 | + | if w.floatFocus || w.stashFocused() || w.Focus < 0 || w.Focus >= len(w.Columns) { | |
| 36 | + | return | |
| 37 | + | } | |
| 38 | + | col := w.Columns[w.Focus] | |
| 39 | + | id := col.Windows[col.Focus] | |
| 40 | + | if len(w.recent) > 0 && w.columnOf(w.recent[0]) == w.Focus { | |
| 41 | + | w.recent[0] = id | |
| 42 | + | return | |
| 43 | + | } | |
| 44 | + | w.recent = slices.DeleteFunc(w.recent, func(v WindowID) bool { return v == id || w.columnOf(v) == w.Focus }) | |
| 45 | + | w.recent = slices.Insert(w.recent, 0, id) | |
| 46 | + | if len(w.recent) > switcherRecentMax { | |
| 47 | + | w.recent = w.recent[:switcherRecentMax] | |
| 48 | + | } | |
| 49 | + | } | |
| 50 | + | ||
| 51 | + | // switchOrder lists one window per column, the focused window of each: the | |
| 52 | + | // focused column first, then the others by recent use, then those never | |
| 53 | + | // used by position. It is nil with fewer than two columns. | |
| 54 | + | func (w *Workspace) switchOrder() []WindowID { | |
| 55 | + | n := len(w.Columns) | |
| 56 | + | if n < 2 || w.Focus < 0 || w.Focus >= n { | |
| 57 | + | return nil | |
| 58 | + | } | |
| 59 | + | seen := make([]bool, n) | |
| 60 | + | order := make([]WindowID, 0, n) | |
| 61 | + | add := func(i int) { | |
| 62 | + | if seen[i] { | |
| 63 | + | return | |
| 64 | + | } | |
| 65 | + | seen[i] = true | |
| 66 | + | c := w.Columns[i] | |
| 67 | + | order = append(order, c.Windows[c.Focus]) | |
| 68 | + | } | |
| 69 | + | add(w.Focus) | |
| 70 | + | for _, id := range w.recent { | |
| 71 | + | if i := w.columnOf(id); i >= 0 { | |
| 72 | + | add(i) | |
| 73 | + | } | |
| 74 | + | } | |
| 75 | + | for i := range n { | |
| 76 | + | add(i) | |
| 77 | + | } | |
| 78 | + | return order | |
| 79 | + | } | |
| @@ -0,0 +1,79 @@ | |||||
| 1 | + | package core | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "slices" | |||
| 5 | + | ) | |||
| 6 | + | ||||
| 7 | + | // The column switcher is an Alt+Tab for the tiled columns of the workspace on | |||
| 8 | + | // screen. Each workspace keeps the columns it focused, most recent first | |||
| 9 | + | // (recent); the switcher steps through them without moving the focus, and | |||
| 10 | + | // commits the selected one when the command key is released. | |||
| 11 | + | ||||
| 12 | + | // switcherRecentMax caps a workspace's recent list. | |||
| 13 | + | const switcherRecentMax = 64 | |||
| 14 | + | ||||
| 15 | + | // switcherState is the switcher of a monitor. A window stands for each | |||
| 16 | + | // column in order, the focused column first. Nothing moves until the commit; | |||
| 17 | + | // shown is set once the cards are drawn. | |||
| 18 | + | type switcherState struct { | |||
| 19 | + | open, shown bool | |||
| 20 | + | ws *Workspace | |||
| 21 | + | order []WindowID | |||
| 22 | + | at int | |||
| 23 | + | // sizes are the client sizes the cards are scaled from. | |||
| 24 | + | sizes map[WindowID]Rect | |||
| 25 | + | } | |||
| 26 | + | ||||
| 27 | + | // previewing reports whether the monitor draws previews instead of its | |||
| 28 | + | // workspace: the overview, or the switcher's cards. | |||
| 29 | + | func (m *Monitor) previewing() bool { return m.ov.open || m.sw.shown } | |||
| 30 | + | ||||
| 31 | + | // noteFocus records the focused column as the most recently used. It does | |||
| 32 | + | // nothing while a float or the stash has the focus, and allocates nothing | |||
| 33 | + | // when the focused column is already the latest. | |||
| 34 | + | func (w *Workspace) noteFocus() { | |||
| 35 | + | if w.floatFocus || w.stashFocused() || w.Focus < 0 || w.Focus >= len(w.Columns) { | |||
| 36 | + | return | |||
| 37 | + | } | |||
| 38 | + | col := w.Columns[w.Focus] | |||
| 39 | + | id := col.Windows[col.Focus] | |||
| 40 | + | if len(w.recent) > 0 && w.columnOf(w.recent[0]) == w.Focus { | |||
| 41 | + | w.recent[0] = id | |||
| 42 | + | return | |||
| 43 | + | } | |||
| 44 | + | w.recent = slices.DeleteFunc(w.recent, func(v WindowID) bool { return v == id || w.columnOf(v) == w.Focus }) | |||
| 45 | + | w.recent = slices.Insert(w.recent, 0, id) | |||
| 46 | + | if len(w.recent) > switcherRecentMax { | |||
| 47 | + | w.recent = w.recent[:switcherRecentMax] | |||
| 48 | + | } | |||
| 49 | + | } | |||
| 50 | + | ||||
| 51 | + | // switchOrder lists one window per column, the focused window of each: the | |||
| 52 | + | // focused column first, then the others by recent use, then those never | |||
| 53 | + | // used by position. It is nil with fewer than two columns. | |||
| 54 | + | func (w *Workspace) switchOrder() []WindowID { | |||
| 55 | + | n := len(w.Columns) | |||
| 56 | + | if n < 2 || w.Focus < 0 || w.Focus >= n { | |||
| 57 | + | return nil | |||
| 58 | + | } | |||
| 59 | + | seen := make([]bool, n) | |||
| 60 | + | order := make([]WindowID, 0, n) | |||
| 61 | + | add := func(i int) { | |||
| 62 | + | if seen[i] { | |||
| 63 | + | return | |||
| 64 | + | } | |||
| 65 | + | seen[i] = true | |||
| 66 | + | c := w.Columns[i] | |||
| 67 | + | order = append(order, c.Windows[c.Focus]) | |||
| 68 | + | } | |||
| 69 | + | add(w.Focus) | |||
| 70 | + | for _, id := range w.recent { | |||
| 71 | + | if i := w.columnOf(id); i >= 0 { | |||
| 72 | + | add(i) | |||
| 73 | + | } | |||
| 74 | + | } | |||
| 75 | + | for i := range n { | |||
| 76 | + | add(i) | |||
| 77 | + | } | |||
| 78 | + | return order | |||
| 79 | + | } | |||
A internal/core/switcher_test.go +129 -0
| @@ -0,0 +1,129 @@ | |||
| 1 | + | package core | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "slices" | |
| 5 | + | "testing" | |
| 6 | + | ) | |
| 7 | + | ||
| 8 | + | // threeColumns is a workspace of three one-window columns, windows 1..3. | |
| 9 | + | func threeColumns() *Workspace { | |
| 10 | + | w := workspace() | |
| 11 | + | w.SetMaxColumns(3) | |
| 12 | + | for id := WindowID(1); id <= 3; id++ { | |
| 13 | + | w.AddWindow(id) | |
| 14 | + | } | |
| 15 | + | return w | |
| 16 | + | } | |
| 17 | + | ||
| 18 | + | func focusNote(w *Workspace, id WindowID) { | |
| 19 | + | w.FocusID(id) | |
| 20 | + | w.noteFocus() | |
| 21 | + | } | |
| 22 | + | ||
| 23 | + | func TestMRUOrderFollowsFocus(t *testing.T) { | |
| 24 | + | w := threeColumns() | |
| 25 | + | // Columns 0, 2, 1 in turn: the last one used goes first. | |
| 26 | + | focusNote(w, 1) | |
| 27 | + | focusNote(w, 3) | |
| 28 | + | focusNote(w, 2) | |
| 29 | + | if got, want := w.switchOrder(), []WindowID{2, 3, 1}; !slices.Equal(got, want) { | |
| 30 | + | t.Fatalf("order %v, want %v", got, want) | |
| 31 | + | } | |
| 32 | + | focusNote(w, 1) | |
| 33 | + | if got, want := w.switchOrder(), []WindowID{1, 2, 3}; !slices.Equal(got, want) { | |
| 34 | + | t.Fatalf("order %v, want %v", got, want) | |
| 35 | + | } | |
| 36 | + | } | |
| 37 | + | ||
| 38 | + | func TestMRUColumnsNeverFocusedFollowByPosition(t *testing.T) { | |
| 39 | + | w := threeColumns() | |
| 40 | + | focusNote(w, 3) | |
| 41 | + | if got, want := w.switchOrder(), []WindowID{3, 1, 2}; !slices.Equal(got, want) { | |
| 42 | + | t.Fatalf("order %v, want %v", got, want) | |
| 43 | + | } | |
| 44 | + | } | |
| 45 | + | ||
| 46 | + | func TestMRUPrunesClosedWindows(t *testing.T) { | |
| 47 | + | w := threeColumns() | |
| 48 | + | focusNote(w, 1) | |
| 49 | + | focusNote(w, 3) | |
| 50 | + | focusNote(w, 2) | |
| 51 | + | w.RemoveWindow(3) | |
| 52 | + | if got, want := w.switchOrder(), []WindowID{2, 1}; !slices.Equal(got, want) { | |
| 53 | + | t.Fatalf("order %v, want %v", got, want) | |
| 54 | + | } | |
| 55 | + | if slices.Contains(w.recent, 3) { | |
| 56 | + | t.Fatalf("recent %v keeps a closed window", w.recent) | |
| 57 | + | } | |
| 58 | + | } | |
| 59 | + | ||
| 60 | + | func TestMRUKeepsRankWhenColumnMoves(t *testing.T) { | |
| 61 | + | w := threeColumns() | |
| 62 | + | focusNote(w, 1) | |
| 63 | + | focusNote(w, 3) | |
| 64 | + | focusNote(w, 2) | |
| 65 | + | // Window 2 moves to the right: its column is the same, so is its rank. | |
| 66 | + | w.MoveColumn(1) | |
| 67 | + | w.noteFocus() | |
| 68 | + | if got, want := w.switchOrder(), []WindowID{2, 3, 1}; !slices.Equal(got, want) { | |
| 69 | + | t.Fatalf("order %v, want %v", got, want) | |
| 70 | + | } | |
| 71 | + | if len(w.recent) != 3 { | |
| 72 | + | t.Fatalf("recent %v", w.recent) | |
| 73 | + | } | |
| 74 | + | } | |
| 75 | + | ||
| 76 | + | func TestMRUOneWindowPerColumn(t *testing.T) { | |
| 77 | + | w := threeColumns() | |
| 78 | + | w.AddWindow(4) | |
| 79 | + | w.FocusColumn(-1) | |
| 80 | + | w.ConsumeOrExpel(-1) | |
| 81 | + | // Whatever the columns became, each appears once. | |
| 82 | + | for range 3 { | |
| 83 | + | w.noteFocus() | |
| 84 | + | w.FocusColumn(-1) | |
| 85 | + | } | |
| 86 | + | order := w.switchOrder() | |
| 87 | + | if len(order) != len(w.Columns) { | |
| 88 | + | t.Fatalf("order %v for %d columns", order, len(w.Columns)) | |
| 89 | + | } | |
| 90 | + | seen := map[int]bool{} | |
| 91 | + | for _, id := range order { | |
| 92 | + | if seen[w.columnOf(id)] { | |
| 93 | + | t.Fatalf("order %v lists a column twice", order) | |
| 94 | + | } | |
| 95 | + | seen[w.columnOf(id)] = true | |
| 96 | + | } | |
| 97 | + | } | |
| 98 | + | ||
| 99 | + | func TestMRUSwitchOrderNeedsTwoColumns(t *testing.T) { | |
| 100 | + | w := workspace() | |
| 101 | + | if w.switchOrder() != nil { | |
| 102 | + | t.Fatal("order of an empty workspace") | |
| 103 | + | } | |
| 104 | + | w.AddWindow(1) | |
| 105 | + | w.noteFocus() | |
| 106 | + | if w.switchOrder() != nil { | |
| 107 | + | t.Fatal("order of one column") | |
| 108 | + | } | |
| 109 | + | } | |
| 110 | + | ||
| 111 | + | func TestMRUCapped(t *testing.T) { | |
| 112 | + | w := workspace() | |
| 113 | + | w.SetMaxColumns(3) | |
| 114 | + | for id := WindowID(1); id <= switcherRecentMax+10; id++ { | |
| 115 | + | w.AddWindow(id) | |
| 116 | + | w.noteFocus() | |
| 117 | + | } | |
| 118 | + | if len(w.recent) != switcherRecentMax { | |
| 119 | + | t.Fatalf("recent holds %d", len(w.recent)) | |
| 120 | + | } | |
| 121 | + | } | |
| 122 | + | ||
| 123 | + | func TestMRUNoteFocusDoesNotAllocateWhenSettled(t *testing.T) { | |
| 124 | + | w := threeColumns() | |
| 125 | + | focusNote(w, 2) | |
| 126 | + | if n := testing.AllocsPerRun(100, w.noteFocus); n != 0 { | |
| 127 | + | t.Fatalf("noteFocus allocates %v", n) | |
| 128 | + | } | |
| 129 | + | } | |
| @@ -0,0 +1,129 @@ | |||||
| 1 | + | package core | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "slices" | |||
| 5 | + | "testing" | |||
| 6 | + | ) | |||
| 7 | + | ||||
| 8 | + | // threeColumns is a workspace of three one-window columns, windows 1..3. | |||
| 9 | + | func threeColumns() *Workspace { | |||
| 10 | + | w := workspace() | |||
| 11 | + | w.SetMaxColumns(3) | |||
| 12 | + | for id := WindowID(1); id <= 3; id++ { | |||
| 13 | + | w.AddWindow(id) | |||
| 14 | + | } | |||
| 15 | + | return w | |||
| 16 | + | } | |||
| 17 | + | ||||
| 18 | + | func focusNote(w *Workspace, id WindowID) { | |||
| 19 | + | w.FocusID(id) | |||
| 20 | + | w.noteFocus() | |||
| 21 | + | } | |||
| 22 | + | ||||
| 23 | + | func TestMRUOrderFollowsFocus(t *testing.T) { | |||
| 24 | + | w := threeColumns() | |||
| 25 | + | // Columns 0, 2, 1 in turn: the last one used goes first. | |||
| 26 | + | focusNote(w, 1) | |||
| 27 | + | focusNote(w, 3) | |||
| 28 | + | focusNote(w, 2) | |||
| 29 | + | if got, want := w.switchOrder(), []WindowID{2, 3, 1}; !slices.Equal(got, want) { | |||
| 30 | + | t.Fatalf("order %v, want %v", got, want) | |||
| 31 | + | } | |||
| 32 | + | focusNote(w, 1) | |||
| 33 | + | if got, want := w.switchOrder(), []WindowID{1, 2, 3}; !slices.Equal(got, want) { | |||
| 34 | + | t.Fatalf("order %v, want %v", got, want) | |||
| 35 | + | } | |||
| 36 | + | } | |||
| 37 | + | ||||
| 38 | + | func TestMRUColumnsNeverFocusedFollowByPosition(t *testing.T) { | |||
| 39 | + | w := threeColumns() | |||
| 40 | + | focusNote(w, 3) | |||
| 41 | + | if got, want := w.switchOrder(), []WindowID{3, 1, 2}; !slices.Equal(got, want) { | |||
| 42 | + | t.Fatalf("order %v, want %v", got, want) | |||
| 43 | + | } | |||
| 44 | + | } | |||
| 45 | + | ||||
| 46 | + | func TestMRUPrunesClosedWindows(t *testing.T) { | |||
| 47 | + | w := threeColumns() | |||
| 48 | + | focusNote(w, 1) | |||
| 49 | + | focusNote(w, 3) | |||
| 50 | + | focusNote(w, 2) | |||
| 51 | + | w.RemoveWindow(3) | |||
| 52 | + | if got, want := w.switchOrder(), []WindowID{2, 1}; !slices.Equal(got, want) { | |||
| 53 | + | t.Fatalf("order %v, want %v", got, want) | |||
| 54 | + | } | |||
| 55 | + | if slices.Contains(w.recent, 3) { | |||
| 56 | + | t.Fatalf("recent %v keeps a closed window", w.recent) | |||
| 57 | + | } | |||
| 58 | + | } | |||
| 59 | + | ||||
| 60 | + | func TestMRUKeepsRankWhenColumnMoves(t *testing.T) { | |||
| 61 | + | w := threeColumns() | |||
| 62 | + | focusNote(w, 1) | |||
| 63 | + | focusNote(w, 3) | |||
| 64 | + | focusNote(w, 2) | |||
| 65 | + | // Window 2 moves to the right: its column is the same, so is its rank. | |||
| 66 | + | w.MoveColumn(1) | |||
| 67 | + | w.noteFocus() | |||
| 68 | + | if got, want := w.switchOrder(), []WindowID{2, 3, 1}; !slices.Equal(got, want) { | |||
| 69 | + | t.Fatalf("order %v, want %v", got, want) | |||
| 70 | + | } | |||
| 71 | + | if len(w.recent) != 3 { | |||
| 72 | + | t.Fatalf("recent %v", w.recent) | |||
| 73 | + | } | |||
| 74 | + | } | |||
| 75 | + | ||||
| 76 | + | func TestMRUOneWindowPerColumn(t *testing.T) { | |||
| 77 | + | w := threeColumns() | |||
| 78 | + | w.AddWindow(4) | |||
| 79 | + | w.FocusColumn(-1) | |||
| 80 | + | w.ConsumeOrExpel(-1) | |||
| 81 | + | // Whatever the columns became, each appears once. | |||
| 82 | + | for range 3 { | |||
| 83 | + | w.noteFocus() | |||
| 84 | + | w.FocusColumn(-1) | |||
| 85 | + | } | |||
| 86 | + | order := w.switchOrder() | |||
| 87 | + | if len(order) != len(w.Columns) { | |||
| 88 | + | t.Fatalf("order %v for %d columns", order, len(w.Columns)) | |||
| 89 | + | } | |||
| 90 | + | seen := map[int]bool{} | |||
| 91 | + | for _, id := range order { | |||
| 92 | + | if seen[w.columnOf(id)] { | |||
| 93 | + | t.Fatalf("order %v lists a column twice", order) | |||
| 94 | + | } | |||
| 95 | + | seen[w.columnOf(id)] = true | |||
| 96 | + | } | |||
| 97 | + | } | |||
| 98 | + | ||||
| 99 | + | func TestMRUSwitchOrderNeedsTwoColumns(t *testing.T) { | |||
| 100 | + | w := workspace() | |||
| 101 | + | if w.switchOrder() != nil { | |||
| 102 | + | t.Fatal("order of an empty workspace") | |||
| 103 | + | } | |||
| 104 | + | w.AddWindow(1) | |||
| 105 | + | w.noteFocus() | |||
| 106 | + | if w.switchOrder() != nil { | |||
| 107 | + | t.Fatal("order of one column") | |||
| 108 | + | } | |||
| 109 | + | } | |||
| 110 | + | ||||
| 111 | + | func TestMRUCapped(t *testing.T) { | |||
| 112 | + | w := workspace() | |||
| 113 | + | w.SetMaxColumns(3) | |||
| 114 | + | for id := WindowID(1); id <= switcherRecentMax+10; id++ { | |||
| 115 | + | w.AddWindow(id) | |||
| 116 | + | w.noteFocus() | |||
| 117 | + | } | |||
| 118 | + | if len(w.recent) != switcherRecentMax { | |||
| 119 | + | t.Fatalf("recent holds %d", len(w.recent)) | |||
| 120 | + | } | |||
| 121 | + | } | |||
| 122 | + | ||||
| 123 | + | func TestMRUNoteFocusDoesNotAllocateWhenSettled(t *testing.T) { | |||
| 124 | + | w := threeColumns() | |||
| 125 | + | focusNote(w, 2) | |||
| 126 | + | if n := testing.AllocsPerRun(100, w.noteFocus); n != 0 { | |||
| 127 | + | t.Fatalf("noteFocus allocates %v", n) | |||
| 128 | + | } | |||
| 129 | + | } | |||