feat/data-dnd e05cb15

viewport_test.go RawHistoryFolder

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}

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 25

Tags 10

↑↓ moveenter openesc close