1package core
2
3import (
4 "context"
5 "slices"
6 "testing"
7
8 "github.com/bnema/neferwl/internal/ports"
9)
10
11func TestResized(t *testing.T) {
12 u := Rect{W: 1000, H: 800}
13 r := Rect{X: 100, Y: 100, W: 300, H: 200}
14 for _, tc := range []struct {
15 edges ports.ResizeEdges
16 dx, dy int
17 want Rect
18 }{
19 {ports.ResizeRight | ports.ResizeBottom, 50, 20, Rect{X: 100, Y: 100, W: 350, H: 220}},
20 {ports.ResizeLeft | ports.ResizeTop, 50, 20, Rect{X: 150, Y: 120, W: 250, H: 180}},
21 {ports.ResizeLeft, -500, 0, Rect{X: 0, Y: 100, W: 400, H: 200}},
22 {ports.ResizeRight, -1000, 0, Rect{X: 100, Y: 100, W: 64, H: 200}},
23 {ports.ResizeBottom, 0, 2000, Rect{X: 100, Y: 100, W: 300, H: 700}},
24 } {
25 if got := resized(r, tc.edges, tc.dx, tc.dy, 64, u); got != tc.want {
26 t.Errorf("%v %d,%d: %+v, want %+v", tc.edges, tc.dx, tc.dy, got, tc.want)
27 }
28 }
29}
30
31func TestScrollBy(t *testing.T) {
32 w := workspace()
33 w.SetOutput(1000, 800)
34 w.SetGaps(10)
35 for id := WindowID(1); id <= 4; id++ {
36 w.AddWindow(id)
37 }
38 w.FocusID(1)
39 if w.View != 0 {
40 t.Fatal(w.View)
41 }
42 if w.scrollBy(-1) {
43 t.Fatal("scrolled past the start")
44 }
45 steps := 0
46 for w.scrollBy(1) {
47 steps++
48 if steps > 10 {
49 t.Fatal("no end")
50 }
51 }
52 if steps != 2 {
53 t.Fatal(steps)
54 }
55 last := w.columnX(3) + w.columnWidth(3) - w.View
56 if last != w.Usable.X+w.Usable.W-w.gap() {
57 t.Fatal(last)
58 }
59 // Focus did not change.
60 if id, _ := w.Focused(); id != 1 {
61 t.Fatal(id)
62 }
63 for w.scrollBy(-1) {
64 }
65 if w.View != 0 {
66 t.Fatal(w.View)
67 }
68}
69
70func TestDropInHiddenColumns(t *testing.T) {
71 w := workspace()
72 w.SetOutput(1000, 800)
73 w.SetGaps(10)
74 w.Overflow = OverflowFixed
75 w.SetMaxColumns(3)
76 for id := WindowID(1); id <= 3; id++ {
77 w.AddWindow(id)
78 }
79 w.FocusID(2)
80 w.ToggleFullWidth()
81 // Columns 1 and 3 are hidden by the maximized one: the point where
82 // column 3 was hits 2 now, a swap with itself: nothing.
83 if tgt := w.dropIn(2, 900, 400, 4); tgt.kind != dropNone {
84 t.Fatalf("%+v", tgt)
85 }
86 // Dragging 1 (hidden) over 2: top band stacks above 2.
87 if tgt := w.dropIn(1, 500, 30, 4); tgt.kind != dropAbove || tgt.anchor != 2 {
88 t.Fatalf("%+v", tgt)
89 }
90}
91
92// dragCore is a core with one 1000x600 output and no channels but the
93// scene and command ones; tests drive its methods directly.
94func dragCore(t *testing.T) *Core {
95 t.Helper()
96 cfg := ports.Config{}
97 cfg.Keyboard.CmdKey = "super"
98 cfg.Layout.MaxColumns = 3
99 c, err := New(cfg, Channels{Scenes: make(chan []ports.Scene, 1), Commands: make(chan ports.ClientCommand, 1024)}, Options{})
100 if err != nil {
101 t.Fatal(err)
102 }
103 c.cur().info = ports.OutputInfo{Name: "OUT", Width: 1000, Height: 600}
104 c.cur().mon.SetOutput(1000, 600)
105 return c
106}
107
108func TestCancelKeepsFloatRaised(t *testing.T) {
109 c := dragCore(t)
110 w := c.cur().mon.Current()
111 w.AddWindow(1)
112 w.AddFloating(2, 1000, 600)
113 w.FocusID(1)
114 if !w.Floats[0].below {
115 t.Fatal("not below")
116 }
117 if err := c.beginDrag(context.Background(), 2, btnLeft, false, 0); err != nil {
118 t.Fatal(err)
119 }
120 c.cancelDrag()
121 if w.Floats[0].below {
122 t.Fatal("escape put the focused float behind the tiles")
123 }
124}
125
126func TestDropReleasesSlot(t *testing.T) {
127 c := dragCore(t)
128 w := c.cur().mon.Current()
129 w.Columns = []Column{{Windows: []WindowID{1}, Slot: 1}, {Windows: []WindowID{2}}}
130 key := slotKey{workspace: w.Name, index: 1}
131 c.slots[key] = &slotState{window: 1}
132 ctx := context.Background()
133 if err := c.beginDrag(ctx, 1, btnLeft, false, 0); err != nil {
134 t.Fatal(err)
135 }
136 c.drag.target = dropTarget{}
137 r, _ := w.placed(2)
138 c.cursorX, c.cursorY = float64(r.X+r.W/2), float64(r.Y+5)
139 if err := c.endDrag(ctx); err != nil {
140 t.Fatal(err)
141 }
142 if !slices.Equal(w.Columns[0].Windows, []WindowID{1, 2}) || w.Columns[0].Slot != 0 {
143 t.Fatal(w.Columns)
144 }
145 if c.slots[key].window != 0 {
146 t.Fatal("slot still holds the window")
147 }
148}