1package core
2
3import (
4 "context"
5 "slices"
6 "testing"
7
8 "github.com/bnema/neferwl/internal/ports"
9)
10
11// A tile is seen in the usable area; a float, a fullscreen window and a
12// preview are placed against the whole frame.
13func TestViewportShows(t *testing.T) {
14 v := viewport{frame: Rect{W: 100, H: 80}, usable: Rect{W: 100, H: 60}}
15 under := Rect{Y: 60, W: 50, H: 60} // the next cascade band, under a bottom panel
16 for _, tc := range []struct {
17 name string
18 v viewport
19 p Placement
20 want bool
21 }{
22 {"tile in usable", v, Placement{Rect: Rect{W: 50, H: 60}}, true},
23 {"tile under panel", v, Placement{Rect: under}, false},
24 {"tile across the panel line", v, Placement{Rect: Rect{Y: 30, W: 50, H: 60}}, true},
25 {"tile off frame", v, Placement{Rect: Rect{Y: 80, W: 50, H: 60}}, false},
26 {"hidden tile", v, Placement{Rect: Rect{W: 50, H: 60}, Hidden: true}, false},
27 {"float under panel", v, Placement{Rect: under, Floating: true}, true},
28 {"hidden float", v, Placement{Rect: Rect{W: 50, H: 60}, Floating: true, Hidden: true}, false},
29 {"fullscreen tile", v, Placement{Rect: Rect{W: 100, H: 80}, Fullscreen: true}, true},
30 {"preview under panel", v, Placement{Rect: under, Preview: 0.5}, true},
31 {"tile with no usable area", viewport{frame: v.frame}, Placement{Rect: Rect{W: 50, H: 60}}, false},
32 {"float with no usable area", viewport{frame: v.frame}, Placement{Rect: Rect{W: 50, H: 60}, Floating: true}, true},
33 } {
34 if got := tc.v.shows(tc.p); got != tc.want {
35 t.Errorf("%s: shows %v, want %v", tc.name, got, tc.want)
36 }
37 }
38}
39
40// viewportCore is a core with one 100x80 output whose usable area is
41// usable, publishing into the returned check: it asserts what each
42// window's configure and the state snapshot say about its visibility.
43func viewportCore(t *testing.T, layout func(*ports.Config), usable Rect) (*Core, func(map[WindowID]bool) ports.Scene) {
44 t.Helper()
45 cfg := ports.Config{}
46 cfg.Keyboard.CmdKey = "super"
47 layout(&cfg)
48 commands := make(chan ports.ClientCommand, 256)
49 scenes := make(chan []ports.Scene, 1)
50 c, err := New(cfg, Channels{Commands: commands, Scenes: scenes}, Options{})
51 if err != nil {
52 t.Fatal(err)
53 }
54 c.addScreen(ports.OutputInfo{Name: "OUT-1", Width: 100, Height: 80})
55 c.cur().mon.SetUsable(usable)
56 sent := map[WindowID]ports.ConfigureWindow{}
57 check := func(want map[WindowID]bool) ports.Scene {
58 t.Helper()
59 if err := c.publish(context.Background()); err != nil {
60 t.Fatal(err)
61 }
62 scene := (<-scenes)[0]
63 for len(commands) > 0 {
64 if v, ok := (<-commands).(ports.ConfigureWindow); ok {
65 sent[v.ID] = v
66 }
67 }
68 state := map[WindowID]bool{}
69 for _, s := range c.state().Windows {
70 state[s.ID] = s.Visible
71 }
72 for id, vis := range want {
73 if v := sent[id]; v.Visible != vis {
74 t.Fatalf("window %d: configure %+v, want visible %v", id, v, vis)
75 }
76 if state[id] != vis {
77 t.Fatalf("window %d: state visible %v, want %v", id, state[id], vis)
78 }
79 }
80 return scene
81 }
82 return c, check
83}
84
85// The next cascade band starts right under a bottom panel: its windows are
86// not seen, so they are told they are invisible (and throttled), while the
87// scene still carries them.
88func TestConfigureVisibleCascadeUnderPanel(t *testing.T) {
89 c, check := viewportCore(t, func(cfg *ports.Config) {
90 cfg.Layout.Overflow = "cascade"
91 cfg.Layout.MaxColumns = 2
92 }, Rect{W: 100, H: 60})
93 m := c.cur().mon
94 w := m.Current()
95 for id := WindowID(1); id <= 3; id++ {
96 m.AddWindow(id)
97 }
98 w.FocusID(1)
99 scene := check(map[WindowID]bool{1: true, 2: true, 3: false})
100 found := false
101 for _, sw := range scene.Windows {
102 found = found || sw.ID == 3
103 }
104 if !found {
105 t.Fatal("window under the panel left the scene")
106 }
107 // It is cut at the panel and takes no pointer there.
108 if scene.TileInset != (ports.Insets{Bottom: 20}) {
109 t.Fatalf("tile inset %+v, want the panel's", scene.TileInset)
110 }
111 if id, _, _ := c.hit(10, 70); id != 0 {
112 t.Fatalf("pointer over the panel went to window %d", id)
113 }
114 if id, _, _ := c.hit(10, 30); id != 1 {
115 t.Fatalf("pointer over window 1 went to %d", id)
116 }
117 // Not on screen: its popups are not either, and it loses the pointer.
118 c.popups[10] = &popupState{id: 10, parent: 3, mapped: true, rect: Rect{W: 5, H: 5}}
119 c.popupOrder = append(c.popupOrder, 10)
120 c.pointer = 3
121 check(nil)
122 if c.visible(3) || c.visible(10) || c.pointer != 0 {
123 t.Fatalf("tile under the panel: visible %v, popup %v, pointer %d", c.visible(3), c.visible(10), c.pointer)
124 }
125 delete(c.popups, 10)
126 c.popupOrder = slices.DeleteFunc(c.popupOrder, func(id WindowID) bool { return id == 10 })
127 // The overview lays previews over the whole output: no inset.
128 m.ToggleOverview()
129 if s := check(nil); s.TileInset != (ports.Insets{}) {
130 t.Fatalf("tile inset %+v in the overview", s.TileInset)
131 }
132 m.ToggleOverview()
133 // Its band scrolled in, the first band is under nothing but off screen.
134 w.FocusID(3)
135 check(map[WindowID]bool{1: false, 2: false, 3: true})
136 // Scrolled back, its band is under the panel again.
137 w.FocusID(1)
138 check(map[WindowID]bool{1: true, 3: false})
139 // Fullscreen covers the panel: always seen.
140 w.FocusID(3)
141 w.ToggleFullscreen()
142 check(map[WindowID]bool{3: true})
143}
144
145// A tile half under a bottom panel: its lines, its pointer bounds (warp,
146// constraints) and its draw all stop at the panel, and the renderer's tile
147// rule matches core's.
148func TestTileStopsAtPanel(t *testing.T) {
149 c, check := viewportCore(t, func(cfg *ports.Config) {
150 cfg.Layout.MaxColumns = 2
151 cfg.Border.Width = 2
152 }, Rect{W: 100, H: 60})
153 m := c.cur().mon
154 m.AddWindow(1)
155 m.AddWindow(3)
156 m.Current().ToggleWindowStash() // 3 floats
157 m.AddWindow(2)
158 m.Current().FocusID(1)
159 scene := check(map[WindowID]bool{1: true, 2: true, 3: true})
160 // Move tile 1 across the panel line, as a slide would.
161 sc := c.cur()
162 sc.shown = slices.Clone(sc.shownLayout())
163 for i := range sc.shown {
164 if sc.shown[i].ID == 1 {
165 sc.shown[i].Rect.Y = 30
166 }
167 }
168 if r, ok := c.shownClientRect(1); !ok || r.Y+r.H > 60 {
169 t.Fatalf("pointer bounds %+v %v reach over the panel", r, ok)
170 }
171 for _, s := range separators(sc.shown, 2, 0, m.viewport(), true) {
172 if s.Window == 0 && s.Rect.Y+s.Rect.H > 60 {
173 t.Fatalf("tile line over the panel: %+v", s)
174 }
175 }
176 view := m.viewport()
177 for k, sw := range scene.Windows {
178 if sw.Popup {
179 continue
180 }
181 if p := sc.settledLayout[k]; sw.Tile() != (view.area(p) == view.usable) {
182 t.Fatalf("window %d: renderer tile %v, core area %+v", sw.ID, sw.Tile(), view.area(p))
183 }
184 }
185}
186
187// Panels that leave no room draw no tile, a leaving one included, and the
188// focus pulse has nothing to show.
189func TestNoUsableAreaDrawsNoTile(t *testing.T) {
190 c, check := viewportCore(t, func(cfg *ports.Config) { cfg.Layout.MaxColumns = 1 }, Rect{})
191 c.cur().mon.AddWindow(1)
192 c.pulse.target = 1
193 scene := check(map[WindowID]bool{1: false})
194 if scene.Draws(scene.Windows[0]) || c.pulse.drawable {
195 t.Fatalf("tile drawn %v, pulse drawable %v with no usable area", scene.Draws(scene.Windows[0]), c.pulse.drawable)
196 }
197 sc := c.cur()
198 sc.shown = slices.Clone(sc.shownLayout())
199 sc.shown[0].Leaving = true
200 scene, _, err := c.sceneFor(context.Background(), 0, sc, nil, nil, 0)
201 if err != nil {
202 t.Fatal(err)
203 }
204 if scene.Draws(scene.Windows[0]) {
205 t.Fatalf("leaving tile drawn with no usable area: %+v", scene.Windows[0])
206 }
207}
208
209// A sized workspace with a panel: the scene's tile area is the workspace's
210// usable area, inside its viewport.
211func TestTileAreaSizedWorkspace(t *testing.T) {
212 c, check := viewportCore(t, func(cfg *ports.Config) { cfg.Layout.MaxColumns = 1 }, Rect{W: 100, H: 70})
213 m := c.cur().mon
214 m.Current().SetSize(60, 80)
215 m.AddWindow(1)
216 scene := check(map[WindowID]bool{1: true})
217 a, ok := scene.TileArea()
218 if u := m.Current().Usable; !ok || a != u || u == m.Current().Output {
219 t.Fatalf("tile area %+v %v, want usable %+v inside frame %+v", a, ok, u, m.Current().Output)
220 }
221}
222
223// A column scrolled under a left panel is not seen.
224func TestConfigureVisibleScrollUnderSidePanel(t *testing.T) {
225 c, check := viewportCore(t, func(cfg *ports.Config) {
226 cfg.Layout.MaxColumns = 1
227 }, Rect{X: 20, W: 80, H: 80})
228 m := c.cur().mon
229 m.AddWindow(1)
230 m.AddWindow(2)
231 if r := m.Layout()[0].Rect; r.X >= 20 || r.X+r.W <= 0 {
232 t.Fatalf("column 1 at %+v, want partly under the panel", r)
233 }
234 check(map[WindowID]bool{1: false, 2: true})
235}