fix(core): identify span lanes by stamp
- commit
- d72b69d08518a4267d0d4eee76be71206b492321
- author
- brice · · 2026-10-21 05:45 UTC
- committer
- brice · · 2026-10-21 07:11 UTC
- parent
- 9f44b24
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
7 files · +84 -15
M internal/core/border.go +2 -1
| @@ -132,10 +132,11 @@ func (w *Workspace) setBorder(b border, pos int) { | |||
| 132 | 132 | return | |
| 133 | 133 | } | |
| 134 | 134 | if !w.validSpans(first, last) { | |
| 135 | + | w.spanSeq++ | |
| 135 | 136 | used := 0 | |
| 136 | 137 | for i := first; i <= last; i++ { | |
| 137 | 138 | w.Columns[i].Span = max((w.columnWidthFor(i, true)*10000+5000)/avail, 1) | |
| 138 | - | w.Columns[i].spanLane = last - first + 1 | |
| 139 | + | w.Columns[i].spanStamp = w.spanSeq | |
| 139 | 140 | used += w.Columns[i].Span | |
| 140 | 141 | } | |
| 141 | 142 | if w.policy().equalCells { | |
| @@ -132,10 +132,11 @@ func (w *Workspace) setBorder(b border, pos int) { | |||||
| 132 | return | 132 | return | ||
| 133 | } | 133 | } | ||
| 134 | if !w.validSpans(first, last) { | 134 | if !w.validSpans(first, last) { | ||
| 135 | + | w.spanSeq++ | |||
| 135 | used := 0 | 136 | used := 0 | ||
| 136 | for i := first; i <= last; i++ { | 137 | for i := first; i <= last; i++ { | ||
| 137 | w.Columns[i].Span = max((w.columnWidthFor(i, true)*10000+5000)/avail, 1) | 138 | w.Columns[i].Span = max((w.columnWidthFor(i, true)*10000+5000)/avail, 1) | ||
| 138 | - | w.Columns[i].spanLane = last - first + 1 | 139 | + | w.Columns[i].spanStamp = w.spanSeq |
| 139 | used += w.Columns[i].Span | 140 | used += w.Columns[i].Span | ||
| 140 | } | 141 | } | ||
| 141 | if w.policy().equalCells { | 142 | if w.policy().equalCells { | ||
M internal/core/border_resize.go +1 -1
| @@ -85,6 +85,6 @@ func (g *borderResize) cancel() { | |||
| 85 | 85 | } | |
| 86 | 86 | for i, saved := range g.saved { | |
| 87 | 87 | col := &g.ws.Columns[i] | |
| 88 | - | col.Span, col.spanLane, col.Shares = saved.Span, saved.spanLane, slices.Clone(saved.Shares) | |
| 88 | + | col.Span, col.spanStamp, col.Shares = saved.Span, saved.spanStamp, slices.Clone(saved.Shares) | |
| 89 | 89 | } | |
| 90 | 90 | } | |
| @@ -85,6 +85,6 @@ func (g *borderResize) cancel() { | |||||
| 85 | } | 85 | } | ||
| 86 | for i, saved := range g.saved { | 86 | for i, saved := range g.saved { | ||
| 87 | col := &g.ws.Columns[i] | 87 | col := &g.ws.Columns[i] | ||
| 88 | - | col.Span, col.spanLane, col.Shares = saved.Span, saved.spanLane, slices.Clone(saved.Shares) | 88 | + | col.Span, col.spanStamp, col.Shares = saved.Span, saved.spanStamp, slices.Clone(saved.Shares) |
| 89 | } | 89 | } | ||
| 90 | } | 90 | } | ||
M internal/core/border_test.go +1 -1
| @@ -106,7 +106,7 @@ func TestSetColumnBorder(t *testing.T) { | |||
| 106 | 106 | } | |
| 107 | 107 | w.resetBorder(b) | |
| 108 | 108 | for _, c := range w.Columns { | |
| 109 | - | if c.Span != 0 || c.spanLane != 0 { | |
| 109 | + | if c.Span != 0 || c.spanStamp != 0 { | |
| 110 | 110 | t.Fatal("reset") | |
| 111 | 111 | } | |
| 112 | 112 | } | |
| @@ -106,7 +106,7 @@ func TestSetColumnBorder(t *testing.T) { | |||||
| 106 | } | 106 | } | ||
| 107 | w.resetBorder(b) | 107 | w.resetBorder(b) | ||
| 108 | for _, c := range w.Columns { | 108 | for _, c := range w.Columns { | ||
| 109 | - | if c.Span != 0 || c.spanLane != 0 { | 109 | + | if c.Span != 0 || c.spanStamp != 0 { |
| 110 | t.Fatal("reset") | 110 | t.Fatal("reset") | ||
| 111 | } | 111 | } | ||
| 112 | } | 112 | } | ||
M internal/core/cascade.go +2 -0
| @@ -511,6 +511,8 @@ func (w *Workspace) spawnIndex() (at int, newBand bool) { | |||
| 511 | 511 | ||
| 512 | 512 | // deleteColumn removes column i; a band it started keeps its start. | |
| 513 | 513 | func (w *Workspace) deleteColumn(i int) { | |
| 514 | + | // A removed member cannot be detected by counting surviving stamp members. | |
| 515 | + | w.clearSpans(i) | |
| 514 | 516 | if w.policy().wraps && w.Columns[i].BandStart && i+1 < len(w.Columns) { | |
| 515 | 517 | w.Columns[i+1].BandStart = true | |
| 516 | 518 | } | |
| @@ -511,6 +511,8 @@ func (w *Workspace) spawnIndex() (at int, newBand bool) { | |||||
| 511 | 511 | ||||
| 512 | // deleteColumn removes column i; a band it started keeps its start. | 512 | // deleteColumn removes column i; a band it started keeps its start. | ||
| 513 | func (w *Workspace) deleteColumn(i int) { | 513 | func (w *Workspace) deleteColumn(i int) { | ||
| 514 | + | // A removed member cannot be detected by counting surviving stamp members. | |||
| 515 | + | w.clearSpans(i) | |||
| 514 | if w.policy().wraps && w.Columns[i].BandStart && i+1 < len(w.Columns) { | 516 | if w.policy().wraps && w.Columns[i].BandStart && i+1 < len(w.Columns) { | ||
| 515 | w.Columns[i+1].BandStart = true | 517 | w.Columns[i+1].BandStart = true | ||
| 516 | } | 518 | } | ||
M internal/core/layout.go +5 -1
| @@ -21,7 +21,7 @@ const maxCells = 16 | |||
| 21 | 21 | type Column struct { | |
| 22 | 22 | // Span is a manually sized width in basis points; zero uses Cells. | |
| 23 | 23 | Span int | |
| 24 | - | spanLane int | |
| 24 | + | spanStamp uint64 | |
| 25 | 25 | Windows []WindowID | |
| 26 | 26 | Cells int | |
| 27 | 27 | FullWidth bool | |
| @@ -108,6 +108,8 @@ type Workspace struct { | |||
| 108 | 108 | overviewAfter *Workspace | |
| 109 | 109 | Overflow Overflow | |
| 110 | 110 | Columns []Column | |
| 111 | + | // spanSeq issues workspace-local identities for complete lane stamps. | |
| 112 | + | spanSeq uint64 | |
| 111 | 113 | // maximized remembers recently maximized columns by window ID, newest | |
| 112 | 114 | // first; stack() filters removed windows at read time. | |
| 113 | 115 | maximized []WindowID | |
| @@ -1037,6 +1039,8 @@ func (w *Workspace) insertColumn(at int, col Column) { | |||
| 1037 | 1039 | // insertAt inserts col at index at; in a cascade it joins the band of the | |
| 1038 | 1040 | // column it goes before. | |
| 1039 | 1041 | func (w *Workspace) insertAt(at int, col Column) { | |
| 1042 | + | // Stamp identities belong to the issuing workspace and complete lane. | |
| 1043 | + | col.Span, col.spanStamp = 0, 0 | |
| 1040 | 1044 | col.BandStart = false | |
| 1041 | 1045 | if w.policy().wraps && at < len(w.Columns) && w.Columns[at].BandStart { | |
| 1042 | 1046 | col.BandStart, w.Columns[at].BandStart = true, false | |
| @@ -21,7 +21,7 @@ const maxCells = 16 | |||||
| 21 | type Column struct { | 21 | type Column struct { | ||
| 22 | // Span is a manually sized width in basis points; zero uses Cells. | 22 | // Span is a manually sized width in basis points; zero uses Cells. | ||
| 23 | Span int | 23 | Span int | ||
| 24 | - | spanLane int | 24 | + | spanStamp uint64 |
| 25 | Windows []WindowID | 25 | Windows []WindowID | ||
| 26 | Cells int | 26 | Cells int | ||
| 27 | FullWidth bool | 27 | FullWidth bool | ||
| @@ -108,6 +108,8 @@ type Workspace struct { | |||||
| 108 | overviewAfter *Workspace | 108 | overviewAfter *Workspace | ||
| 109 | Overflow Overflow | 109 | Overflow Overflow | ||
| 110 | Columns []Column | 110 | Columns []Column | ||
| 111 | + | // spanSeq issues workspace-local identities for complete lane stamps. | |||
| 112 | + | spanSeq uint64 | |||
| 111 | // maximized remembers recently maximized columns by window ID, newest | 113 | // maximized remembers recently maximized columns by window ID, newest | ||
| 112 | // first; stack() filters removed windows at read time. | 114 | // first; stack() filters removed windows at read time. | ||
| 113 | maximized []WindowID | 115 | maximized []WindowID | ||
| @@ -1037,6 +1039,8 @@ func (w *Workspace) insertColumn(at int, col Column) { | |||||
| 1037 | // insertAt inserts col at index at; in a cascade it joins the band of the | 1039 | // insertAt inserts col at index at; in a cascade it joins the band of the | ||
| 1038 | // column it goes before. | 1040 | // column it goes before. | ||
| 1039 | func (w *Workspace) insertAt(at int, col Column) { | 1041 | func (w *Workspace) insertAt(at int, col Column) { | ||
| 1042 | + | // Stamp identities belong to the issuing workspace and complete lane. | |||
| 1043 | + | col.Span, col.spanStamp = 0, 0 | |||
| 1040 | col.BandStart = false | 1044 | col.BandStart = false | ||
| 1041 | if w.policy().wraps && at < len(w.Columns) && w.Columns[at].BandStart { | 1045 | if w.policy().wraps && at < len(w.Columns) && w.Columns[at].BandStart { | ||
| 1042 | col.BandStart, w.Columns[at].BandStart = true, false | 1046 | col.BandStart, w.Columns[at].BandStart = true, false | ||
M internal/core/layout_policy.go +24 -8
| @@ -54,35 +54,51 @@ func (w *Workspace) policy() layoutPolicy { | |||
| 54 | 54 | return layoutPolicy{content: horizontalAxis, workspace: verticalAxis} | |
| 55 | 55 | } | |
| 56 | 56 | ||
| 57 | - | // validSpans rejects partial stamps and stamps from a differently sized lane. | |
| 57 | + | // validSpans requires a complete, exclusive lane stamp: every column has | |
| 58 | + | // a positive span and the same nonzero identity, with no members elsewhere | |
| 59 | + | // in the workspace. Equal-cell lanes must also sum to 10000 basis points. | |
| 58 | 60 | func (w *Workspace) validSpans(first, last int) bool { | |
| 59 | 61 | if first > last || w.Overflow == OverflowFixed && !w.laneRow() { | |
| 60 | 62 | return false | |
| 61 | 63 | } | |
| 64 | + | stamp := w.Columns[first].spanStamp | |
| 65 | + | if stamp == 0 { | |
| 66 | + | return false | |
| 67 | + | } | |
| 62 | 68 | total := 0 | |
| 63 | 69 | for i := first; i <= last; i++ { | |
| 64 | 70 | c := w.Columns[i] | |
| 65 | - | if c.Span <= 0 || c.spanLane != last-first+1 { | |
| 71 | + | if c.Span <= 0 || c.spanStamp != stamp { | |
| 66 | 72 | return false | |
| 67 | 73 | } | |
| 68 | 74 | total += c.Span | |
| 69 | 75 | } | |
| 70 | - | return !w.policy().equalCells || total == 10000 | |
| 76 | + | count := 0 | |
| 77 | + | for _, c := range w.Columns { | |
| 78 | + | if c.spanStamp == stamp { | |
| 79 | + | count++ | |
| 80 | + | } | |
| 81 | + | } | |
| 82 | + | return count == last-first+1 && (!w.policy().equalCells || total == 10000) | |
| 71 | 83 | } | |
| 72 | 84 | ||
| 73 | - | // dropStaleSpans runs before scroll measures mutated layout geometry. A band | |
| 74 | - | // is independent: invalidating one never discards another band's ratios. | |
| 85 | + | // dropStaleSpans runs before scroll measures mutated layout geometry. Decide | |
| 86 | + | // validity before clearing any stamps, so shared identities cannot become | |
| 87 | + | // valid merely because an earlier invalid lane was cleared. | |
| 75 | 88 | func (w *Workspace) dropStaleSpans() { | |
| 89 | + | var stale []int | |
| 76 | 90 | for first := 0; first < len(w.Columns); { | |
| 77 | 91 | _, last := w.laneBounds(w.laneOf(first)) | |
| 78 | 92 | if !w.validSpans(first, last) { | |
| 79 | 93 | for i := first; i <= last; i++ { | |
| 80 | - | w.Columns[i].Span = 0 | |
| 81 | - | w.Columns[i].spanLane = 0 | |
| 94 | + | stale = append(stale, i) | |
| 82 | 95 | } | |
| 83 | 96 | } | |
| 84 | 97 | first = last + 1 | |
| 85 | 98 | } | |
| 99 | + | for _, i := range stale { | |
| 100 | + | w.Columns[i].Span, w.Columns[i].spanStamp = 0, 0 | |
| 101 | + | } | |
| 86 | 102 | } | |
| 87 | 103 | ||
| 88 | 104 | func (w *Workspace) clearSpans(col int) { | |
| @@ -92,6 +108,6 @@ func (w *Workspace) clearSpans(col int) { | |||
| 92 | 108 | first, last := w.laneBounds(w.laneOf(col)) | |
| 93 | 109 | for i := first; i <= last; i++ { | |
| 94 | 110 | w.Columns[i].Span = 0 | |
| 95 | - | w.Columns[i].spanLane = 0 | |
| 111 | + | w.Columns[i].spanStamp = 0 | |
| 96 | 112 | } | |
| 97 | 113 | } | |
| @@ -54,35 +54,51 @@ func (w *Workspace) policy() layoutPolicy { | |||||
| 54 | return layoutPolicy{content: horizontalAxis, workspace: verticalAxis} | 54 | return layoutPolicy{content: horizontalAxis, workspace: verticalAxis} | ||
| 55 | } | 55 | } | ||
| 56 | 56 | ||||
| 57 | - | // validSpans rejects partial stamps and stamps from a differently sized lane. | 57 | + | // validSpans requires a complete, exclusive lane stamp: every column has |
| 58 | + | // a positive span and the same nonzero identity, with no members elsewhere | |||
| 59 | + | // in the workspace. Equal-cell lanes must also sum to 10000 basis points. | |||
| 58 | func (w *Workspace) validSpans(first, last int) bool { | 60 | func (w *Workspace) validSpans(first, last int) bool { | ||
| 59 | if first > last || w.Overflow == OverflowFixed && !w.laneRow() { | 61 | if first > last || w.Overflow == OverflowFixed && !w.laneRow() { | ||
| 60 | return false | 62 | return false | ||
| 61 | } | 63 | } | ||
| 64 | + | stamp := w.Columns[first].spanStamp | |||
| 65 | + | if stamp == 0 { | |||
| 66 | + | return false | |||
| 67 | + | } | |||
| 62 | total := 0 | 68 | total := 0 | ||
| 63 | for i := first; i <= last; i++ { | 69 | for i := first; i <= last; i++ { | ||
| 64 | c := w.Columns[i] | 70 | c := w.Columns[i] | ||
| 65 | - | if c.Span <= 0 || c.spanLane != last-first+1 { | 71 | + | if c.Span <= 0 || c.spanStamp != stamp { |
| 66 | return false | 72 | return false | ||
| 67 | } | 73 | } | ||
| 68 | total += c.Span | 74 | total += c.Span | ||
| 69 | } | 75 | } | ||
| 70 | - | return !w.policy().equalCells || total == 10000 | 76 | + | count := 0 |
| 77 | + | for _, c := range w.Columns { | |||
| 78 | + | if c.spanStamp == stamp { | |||
| 79 | + | count++ | |||
| 80 | + | } | |||
| 81 | + | } | |||
| 82 | + | return count == last-first+1 && (!w.policy().equalCells || total == 10000) | |||
| 71 | } | 83 | } | ||
| 72 | 84 | ||||
| 73 | - | // dropStaleSpans runs before scroll measures mutated layout geometry. A band | 85 | + | // dropStaleSpans runs before scroll measures mutated layout geometry. Decide |
| 74 | - | // is independent: invalidating one never discards another band's ratios. | 86 | + | // validity before clearing any stamps, so shared identities cannot become |
| 87 | + | // valid merely because an earlier invalid lane was cleared. | |||
| 75 | func (w *Workspace) dropStaleSpans() { | 88 | func (w *Workspace) dropStaleSpans() { | ||
| 89 | + | var stale []int | |||
| 76 | for first := 0; first < len(w.Columns); { | 90 | for first := 0; first < len(w.Columns); { | ||
| 77 | _, last := w.laneBounds(w.laneOf(first)) | 91 | _, last := w.laneBounds(w.laneOf(first)) | ||
| 78 | if !w.validSpans(first, last) { | 92 | if !w.validSpans(first, last) { | ||
| 79 | for i := first; i <= last; i++ { | 93 | for i := first; i <= last; i++ { | ||
| 80 | - | w.Columns[i].Span = 0 | 94 | + | stale = append(stale, i) |
| 81 | - | w.Columns[i].spanLane = 0 | |||
| 82 | } | 95 | } | ||
| 83 | } | 96 | } | ||
| 84 | first = last + 1 | 97 | first = last + 1 | ||
| 85 | } | 98 | } | ||
| 99 | + | for _, i := range stale { | |||
| 100 | + | w.Columns[i].Span, w.Columns[i].spanStamp = 0, 0 | |||
| 101 | + | } | |||
| 86 | } | 102 | } | ||
| 87 | 103 | ||||
| 88 | func (w *Workspace) clearSpans(col int) { | 104 | func (w *Workspace) clearSpans(col int) { | ||
| @@ -92,6 +108,6 @@ func (w *Workspace) clearSpans(col int) { | |||||
| 92 | first, last := w.laneBounds(w.laneOf(col)) | 108 | first, last := w.laneBounds(w.laneOf(col)) | ||
| 93 | for i := first; i <= last; i++ { | 109 | for i := first; i <= last; i++ { | ||
| 94 | w.Columns[i].Span = 0 | 110 | w.Columns[i].Span = 0 | ||
| 95 | - | w.Columns[i].spanLane = 0 | 111 | + | w.Columns[i].spanStamp = 0 |
| 96 | } | 112 | } | ||
| 97 | } | 113 | } | ||
M internal/core/spans_test.go +49 -3
| @@ -17,9 +17,10 @@ func spanWorkspace(mode Overflow, n int) *Workspace { | |||
| 17 | 17 | } | |
| 18 | 18 | ||
| 19 | 19 | func stampSpans(w *Workspace, first, last int) { | |
| 20 | + | w.spanSeq++ | |
| 20 | 21 | for i := first; i <= last; i++ { | |
| 21 | 22 | w.Columns[i].Span = 10000 / (last - first + 1) | |
| 22 | - | w.Columns[i].spanLane = last - first + 1 | |
| 23 | + | w.Columns[i].spanStamp = w.spanSeq | |
| 23 | 24 | } | |
| 24 | 25 | w.Columns[last].Span += 10000 % (last - first + 1) | |
| 25 | 26 | } | |
| @@ -64,7 +65,7 @@ func TestSpansMutations(t *testing.T) { | |||
| 64 | 65 | if c.Span == 0 { | |
| 65 | 66 | t.Fatal("swap discarded valid spans") | |
| 66 | 67 | } | |
| 67 | - | } else if c.Span != 0 || c.spanLane != 0 { | |
| 68 | + | } else if c.Span != 0 || c.spanStamp != 0 { | |
| 68 | 69 | t.Fatal("stale span", c) | |
| 69 | 70 | } | |
| 70 | 71 | } | |
| @@ -134,9 +135,54 @@ func TestSpansDropIntoLane(t *testing.T) { | |||
| 134 | 135 | t.Fatal("not dropped", mode, w.Columns) | |
| 135 | 136 | } | |
| 136 | 137 | for _, col := range w.Columns { | |
| 137 | - | if col.Span != 0 || col.spanLane != 0 { | |
| 138 | + | if col.Span != 0 || col.spanStamp != 0 { | |
| 138 | 139 | t.Fatal("drop retained stamp", mode, col) | |
| 139 | 140 | } | |
| 140 | 141 | } | |
| 141 | 142 | } | |
| 142 | 143 | } | |
| 144 | + | ||
| 145 | + | func TestSpanLaneIdentity(t *testing.T) { | |
| 146 | + | for _, action := range []string{"cross-drop", "within-drop", "within-keyboard"} { | |
| 147 | + | t.Run(action, func(t *testing.T) { | |
| 148 | + | w := spanWorkspace(OverflowCascade, 4) | |
| 149 | + | w.SetMaxColumns(2) | |
| 150 | + | for i := range w.Columns { | |
| 151 | + | w.Columns[i].BandStart = i%2 == 0 | |
| 152 | + | } | |
| 153 | + | for _, i := range []int{0, 2} { | |
| 154 | + | rs := w.columnRects() | |
| 155 | + | w.setBorder(border{kind: borderColumn, col: i}, rs[i].X+rs[i].W+5) | |
| 156 | + | } | |
| 157 | + | for _, col := range w.Columns { | |
| 158 | + | if col.Span != 5000 { | |
| 159 | + | t.Fatal("not stamped equally", w.Columns) | |
| 160 | + | } | |
| 161 | + | } | |
| 162 | + | cross := action == "cross-drop" | |
| 163 | + | if action == "cross-drop" || action == "within-drop" { | |
| 164 | + | m := monitor() | |
| 165 | + | m.Workspaces = []*Workspace{w} | |
| 166 | + | sc := &screen{mon: m} | |
| 167 | + | c := &Core{screens: []*screen{sc}} | |
| 168 | + | to := 1 | |
| 169 | + | if cross { | |
| 170 | + | to = 2 | |
| 171 | + | } | |
| 172 | + | c.drop(w.Columns[0].Windows[0], dropTarget{kind: dropSwap, screen: sc, ws: w, anchor: w.Columns[to].Windows[0]}) | |
| 173 | + | } else { | |
| 174 | + | w.Focus = 0 | |
| 175 | + | w.MoveColumn(1) | |
| 176 | + | } | |
| 177 | + | w.dropStaleSpans() | |
| 178 | + | for _, col := range w.Columns { | |
| 179 | + | if cross && col.Span != 0 { | |
| 180 | + | t.Fatal("cross-band swap retained stamp", w.Columns) | |
| 181 | + | } | |
| 182 | + | if !cross && col.Span != 5000 { | |
| 183 | + | t.Fatal("within-band swap discarded stamp", w.Columns) | |
| 184 | + | } | |
| 185 | + | } | |
| 186 | + | }) | |
| 187 | + | } | |
| 188 | + | } | |
| @@ -17,9 +17,10 @@ func spanWorkspace(mode Overflow, n int) *Workspace { | |||||
| 17 | } | 17 | } | ||
| 18 | 18 | ||||
| 19 | func stampSpans(w *Workspace, first, last int) { | 19 | func stampSpans(w *Workspace, first, last int) { | ||
| 20 | + | w.spanSeq++ | |||
| 20 | for i := first; i <= last; i++ { | 21 | for i := first; i <= last; i++ { | ||
| 21 | w.Columns[i].Span = 10000 / (last - first + 1) | 22 | w.Columns[i].Span = 10000 / (last - first + 1) | ||
| 22 | - | w.Columns[i].spanLane = last - first + 1 | 23 | + | w.Columns[i].spanStamp = w.spanSeq |
| 23 | } | 24 | } | ||
| 24 | w.Columns[last].Span += 10000 % (last - first + 1) | 25 | w.Columns[last].Span += 10000 % (last - first + 1) | ||
| 25 | } | 26 | } | ||
| @@ -64,7 +65,7 @@ func TestSpansMutations(t *testing.T) { | |||||
| 64 | if c.Span == 0 { | 65 | if c.Span == 0 { | ||
| 65 | t.Fatal("swap discarded valid spans") | 66 | t.Fatal("swap discarded valid spans") | ||
| 66 | } | 67 | } | ||
| 67 | - | } else if c.Span != 0 || c.spanLane != 0 { | 68 | + | } else if c.Span != 0 || c.spanStamp != 0 { |
| 68 | t.Fatal("stale span", c) | 69 | t.Fatal("stale span", c) | ||
| 69 | } | 70 | } | ||
| 70 | } | 71 | } | ||
| @@ -134,9 +135,54 @@ func TestSpansDropIntoLane(t *testing.T) { | |||||
| 134 | t.Fatal("not dropped", mode, w.Columns) | 135 | t.Fatal("not dropped", mode, w.Columns) | ||
| 135 | } | 136 | } | ||
| 136 | for _, col := range w.Columns { | 137 | for _, col := range w.Columns { | ||
| 137 | - | if col.Span != 0 || col.spanLane != 0 { | 138 | + | if col.Span != 0 || col.spanStamp != 0 { |
| 138 | t.Fatal("drop retained stamp", mode, col) | 139 | t.Fatal("drop retained stamp", mode, col) | ||
| 139 | } | 140 | } | ||
| 140 | } | 141 | } | ||
| 141 | } | 142 | } | ||
| 142 | } | 143 | } | ||
| 144 | + | ||||
| 145 | + | func TestSpanLaneIdentity(t *testing.T) { | |||
| 146 | + | for _, action := range []string{"cross-drop", "within-drop", "within-keyboard"} { | |||
| 147 | + | t.Run(action, func(t *testing.T) { | |||
| 148 | + | w := spanWorkspace(OverflowCascade, 4) | |||
| 149 | + | w.SetMaxColumns(2) | |||
| 150 | + | for i := range w.Columns { | |||
| 151 | + | w.Columns[i].BandStart = i%2 == 0 | |||
| 152 | + | } | |||
| 153 | + | for _, i := range []int{0, 2} { | |||
| 154 | + | rs := w.columnRects() | |||
| 155 | + | w.setBorder(border{kind: borderColumn, col: i}, rs[i].X+rs[i].W+5) | |||
| 156 | + | } | |||
| 157 | + | for _, col := range w.Columns { | |||
| 158 | + | if col.Span != 5000 { | |||
| 159 | + | t.Fatal("not stamped equally", w.Columns) | |||
| 160 | + | } | |||
| 161 | + | } | |||
| 162 | + | cross := action == "cross-drop" | |||
| 163 | + | if action == "cross-drop" || action == "within-drop" { | |||
| 164 | + | m := monitor() | |||
| 165 | + | m.Workspaces = []*Workspace{w} | |||
| 166 | + | sc := &screen{mon: m} | |||
| 167 | + | c := &Core{screens: []*screen{sc}} | |||
| 168 | + | to := 1 | |||
| 169 | + | if cross { | |||
| 170 | + | to = 2 | |||
| 171 | + | } | |||
| 172 | + | c.drop(w.Columns[0].Windows[0], dropTarget{kind: dropSwap, screen: sc, ws: w, anchor: w.Columns[to].Windows[0]}) | |||
| 173 | + | } else { | |||
| 174 | + | w.Focus = 0 | |||
| 175 | + | w.MoveColumn(1) | |||
| 176 | + | } | |||
| 177 | + | w.dropStaleSpans() | |||
| 178 | + | for _, col := range w.Columns { | |||
| 179 | + | if cross && col.Span != 0 { | |||
| 180 | + | t.Fatal("cross-band swap retained stamp", w.Columns) | |||
| 181 | + | } | |||
| 182 | + | if !cross && col.Span != 5000 { | |||
| 183 | + | t.Fatal("within-band swap discarded stamp", w.Columns) | |||
| 184 | + | } | |||
| 185 | + | } | |||
| 186 | + | }) | |||
| 187 | + | } | |||
| 188 | + | } | |||