feat/data-dnd e05cb15

switcher.go RawHistoryFolder

1package core
2
3import (
4	"context"
5	"math"
6	"slices"
7	"time"
8)
9
10// The column switcher is an Alt+Tab for the tiled columns of the workspace on
11// screen. Each workspace keeps the columns it focused, most recent first
12// (recent); the switcher steps through them without moving the focus, and
13// commits the selected one when the command key is released.
14
15// switcherRecentMax caps a workspace's recent list.
16const switcherRecentMax = 64
17
18// switcherDelay is how long the command key must stay held before the cards
19// show: a quick tap swaps at once and flashes nothing.
20const switcherDelay = 150 * time.Millisecond
21
22// switcherState is the switcher of a monitor. A window stands for each
23// column in order, the focused column first. Nothing moves until the commit;
24// shown is set once the cards are drawn.
25type switcherState struct {
26	// ws is the workspace the switcher opened on; nil when closed.
27	ws    *Workspace
28	shown bool
29	order []WindowID
30	at    int
31	// sizes are the client sizes the cards are scaled from.
32	sizes map[WindowID]Rect
33}
34
35// switcherShown reports whether the switcher's cards are drawn. They belong
36// to the workspace that was on screen when they showed: if another one is on
37// screen now (an activation, the taskbar, an output coming back) the cards
38// are stale and the monitor draws its workspace again.
39func (m *Monitor) switcherShown() bool { return m.sw.shown && m.sw.ws == m.Current() }
40
41// previewing reports whether the monitor draws previews instead of its
42// workspace: the overview, or the switcher's cards.
43func (m *Monitor) previewing() bool { return m.ov.open || m.switcherShown() }
44
45// noteFocus records the focused column as the most recently used. It does
46// nothing while a float or the stash has the focus, and allocates nothing
47// when the focused column is already the latest.
48func (w *Workspace) noteFocus() {
49	if w.floatFocus || w.stashFocused() || w.Focus < 0 || w.Focus >= len(w.Columns) {
50		return
51	}
52	col := w.Columns[w.Focus]
53	id := col.Windows[col.Focus]
54	if len(w.recent) > 0 && slices.Contains(col.Windows, w.recent[0]) {
55		w.recent[0] = id
56		return
57	}
58	w.recent = slices.DeleteFunc(w.recent, func(v WindowID) bool { return v == id || w.columnOf(v) == w.Focus })
59	w.recent = slices.Insert(w.recent, 0, id)
60	if len(w.recent) > switcherRecentMax {
61		w.recent = w.recent[:switcherRecentMax]
62	}
63}
64
65// switchOrder lists one window per column, the focused window of each: the
66// focused column first, then the others by recent use, then those never
67// used by position. It is nil with fewer than two columns.
68func (w *Workspace) switchOrder() []WindowID {
69	n := len(w.Columns)
70	if n < 2 || w.Focus < 0 || w.Focus >= n {
71		return nil
72	}
73	seen := make([]bool, n)
74	order := make([]WindowID, 0, n)
75	add := func(i int) {
76		if seen[i] {
77			return
78		}
79		seen[i] = true
80		c := w.Columns[i]
81		order = append(order, c.Windows[c.Focus])
82	}
83	add(w.Focus)
84	for _, id := range w.recent {
85		if i := w.columnOf(id); i >= 0 {
86			add(i)
87		}
88	}
89	for i := range n {
90		add(i)
91	}
92	return order
93}
94
95// heir is the window of id's column that is not id, or 0: call it before id
96// is removed, then hand it to remove.
97func (s *switcherState) heir(id WindowID) WindowID {
98	if s.ws == nil || !slices.Contains(s.order, id) {
99		return 0
100	}
101	if i := s.ws.columnOf(id); i >= 0 {
102		for _, v := range s.ws.Columns[i].Windows {
103			if v != id {
104				return v
105			}
106		}
107	}
108	return 0
109}
110
111// remove drops a closed window from the open switcher. When its column
112// still has windows (heir, from before the removal) the card stands for the
113// column's new focused window and the selection stays; otherwise the card
114// goes, and the selection stays on the same card (or the one that took its
115// place). With fewer than two columns left there is nothing to switch.
116func (s *switcherState) remove(id, heir WindowID) {
117	if s.ws == nil {
118		return
119	}
120	i := slices.Index(s.order, id)
121	if i < 0 {
122		return
123	}
124	if j := s.ws.columnOf(heir); heir != 0 && j >= 0 {
125		col := s.ws.Columns[j]
126		next := col.Windows[col.Focus]
127		if size, ok := s.sizes[id]; ok {
128			if _, has := s.sizes[next]; !has {
129				s.sizes[next] = size
130			}
131		}
132		s.order[i] = next
133		return
134	}
135	s.order = slices.Delete(s.order, i, i+1)
136	if i < s.at {
137		s.at--
138	}
139	s.at = max(min(s.at, len(s.order)-1), 0)
140	if len(s.order) < 2 {
141		*s = switcherState{}
142	}
143}
144
145// commitSwitcher closes the switcher and focuses the selected column. The
146// column left hands its maximization to the target. It does nothing when the
147// workspace is not on screen any more, the window went, or the selection is
148// the tile already focused (a float or the stash holding the focus does not
149// count: the tile takes it back). It reports whether the selection was still
150// valid on the workspace on screen.
151func (m *Monitor) commitSwitcher() bool {
152	s := m.sw
153	m.sw = switcherState{}
154	if s.ws == nil || s.at < 0 || s.at >= len(s.order) {
155		return false
156	}
157	id, w := s.order[s.at], s.ws
158	if w != m.Current() {
159		return false
160	}
161	i := w.columnOf(id)
162	if i < 0 {
163		return false
164	}
165	if i == w.Focus && !w.floatFocus && !w.stashFocused() {
166		return true
167	}
168	if i != w.Focus && !w.policy().wraps && w.Focus >= 0 && w.Focus < len(w.Columns) && w.Columns[w.Focus].FullWidth {
169		w.transferMaximization(i)
170	}
171	// Activate leaves another window's fullscreen and handles dialogs.
172	w.Activate(id)
173	return true
174}
175
176// switchDir is the direction of a switch action, 0 for any other action.
177func switchDir(a Action) int {
178	switch a {
179	case ActionSwitchColumnNext:
180		return 1
181	case ActionSwitchColumnPrev:
182		return -1
183	}
184	return 0
185}
186
187// switching reports whether a switcher is open.
188func (c *Core) switching() bool {
189	return c.switcher.sc != nil && c.switcher.sc.mon.sw.ws != nil
190}
191
192// switcherStale reports whether the open switcher belongs to a workspace
193// that is not on its screen any more.
194func (c *Core) switcherStale() bool {
195	m := c.switcher.sc.mon
196	return m.sw.ws != nil && m.sw.ws != m.Current()
197}
198
199func (c *Core) stopSwitcherTimer() {
200	if c.switcher.timerStop != nil {
201		c.switcher.timerStop()
202	}
203	c.switcher.timerC, c.switcher.timerStop = nil, nil
204}
205
206// cancelSwitcher closes the switcher with the focus unchanged. It only
207// resets state: callers publish.
208func (c *Core) cancelSwitcher() {
209	c.stopSwitcherTimer()
210	if sc := c.switcher.sc; sc != nil {
211		sc.mon.sw = switcherState{}
212	}
213	c.switcher.sc = nil
214}
215
216// commitSwitcher commits the switcher on its own screen and ends it. The
217// keyboard follows the committed window, even if the pointer moved the focus
218// to another output meanwhile.
219func (c *Core) commitSwitcher() {
220	c.stopSwitcherTimer()
221	if sc := c.switcher.sc; sc != nil {
222		if sc.mon.commitSwitcher() {
223			if i := slices.Index(c.screens, sc); i >= 0 {
224				c.focusScreen = i
225			}
226		}
227	}
228	c.switcher.sc = nil
229}
230
231// switchStep steps the switcher by dir (+1 next, -1 previous), opening it
232// on the focused screen first. With the command key held the cards show after
233// switcherDelay and the commit waits for the release; otherwise it commits
234// at once.
235func (c *Core) switchStep(dir int) {
236	sc := c.cur()
237	if c.switching() && (c.switcher.sc != sc || c.switcherStale()) {
238		// The focus moved to another output, or another workspace came on
239		// screen: start over.
240		c.cancelSwitcher()
241	}
242	if sc.mon.ov.open || c.drag != nil {
243		return
244	}
245	m := sc.mon
246	opened := false
247	if !c.switching() {
248		c.cancelSwitcher()
249		w := m.Current()
250		order := w.switchOrder()
251		if order == nil {
252			return
253		}
254		sizes := make(map[WindowID]Rect, len(order))
255		for _, id := range order {
256			if v, ok := c.configures.sent[id]; ok && v.Width > 0 && v.Height > 0 {
257				sizes[id] = Rect{W: v.Width, H: v.Height}
258			}
259		}
260		at := 1
261		if dir < 0 {
262			at = len(order) - 1
263		}
264		m.sw = switcherState{ws: w, order: order, at: at, sizes: sizes}
265		c.switcher.sc = sc
266		opened = true
267	} else {
268		n := len(m.sw.order)
269		m.sw.at = (m.sw.at + dir + n) % n
270	}
271	if held := c.cmdMod != 0 && c.mods&c.cmdMod != 0; !held {
272		c.commitSwitcher()
273		return
274	}
275	if opened {
276		c.switcher.timerC, c.switcher.timerStop = newTimer(c.opts.Clock, switcherDelay)
277	}
278}
279
280// switcherRun runs f, which changes the switcher, between a snapshot and
281// the transition of what the screens showed, then publishes and rehits so
282// the pointer never points at a stale window.
283func (c *Core) switcherRun(ctx context.Context, f func()) error {
284	now := c.now()
285	shots := c.snapshot(now)
286	f()
287	c.transition(shots, now)
288	if err := c.publish(ctx); err != nil {
289		return err
290	}
291	return c.rehit(ctx)
292}
293
294// switchBind runs a switch-column bind.
295func (c *Core) switchBind(ctx context.Context, dir int) error {
296	return c.switcherRun(ctx, func() {
297		c.keyboard.takeBack() // a bind acts on the windows
298		c.switchStep(dir)
299	})
300}
301
302// releaseSwitcher commits the selection once the command key is up.
303func (c *Core) releaseSwitcher(ctx context.Context) error {
304	return c.switcherRun(ctx, c.commitSwitcher)
305}
306
307// abortSwitcher cancels the switcher and publishes the workspace again.
308func (c *Core) abortSwitcher(ctx context.Context) error {
309	return c.switcherRun(ctx, c.cancelSwitcher)
310}
311
312// switcherTick runs when the delay passed with the command key held: the
313// cards show. It reports whether the scene changed.
314func (c *Core) switcherTick() bool {
315	c.switcher.timerC, c.switcher.timerStop = nil, nil
316	sc := c.switcher.sc
317	if !c.switching() {
318		c.switcher.sc = nil
319		return false
320	}
321	m := sc.mon
322	if m.sw.ws != m.Current() {
323		c.cancelSwitcher()
324		return true
325	}
326	now := c.now()
327	shots := c.snapshot(now)
328	m.sw.shown = true
329	c.transition(shots, now)
330	return true
331}
332
333// switcherClick takes a press while the switcher is open. Before the cards
334// show it cancels and lets the press through (handled false). On the cards
335// a press on one commits it, anywhere else cancels; the press and its
336// release are consumed either way.
337func (c *Core) switcherClick(ctx context.Context, button uint32) (handled bool, err error) {
338	sc := c.switcher.sc
339	if !c.switching() {
340		return false, nil
341	}
342	if !sc.mon.sw.shown {
343		c.cancelSwitcher()
344		return false, nil
345	}
346	err = c.switcherRun(ctx, func() {
347		id := overviewIn(sc.shownLayout(), c.cursorX-float64(sc.x), c.cursorY-float64(sc.y))
348		if i := slices.Index(sc.mon.sw.order, id); id != 0 && i >= 0 {
349			sc.mon.sw.at = i
350			c.commitSwitcher()
351		} else {
352			c.cancelSwitcher()
353		}
354		c.buttons[button], c.swallow[button] = true, true
355	})
356	return true, err
357}
358
359// switcherLayout lays out the shown switcher: one card per column in
360// switchOrder, equal boxes in a row centered in the overview area, every
361// window scaled into its box (never up). Like the overview, the selected
362// card gets Focused (so its configure is Activated) while the keyboard stays
363// on the real focus. Every other window is hidden.
364func (m *Monitor) switcherLayout(dst []Placement) []Placement {
365	sw := &m.sw
366	w := sw.ws
367	area := w.overviewArea()
368	n := len(sw.order)
369	for ws := range m.all() {
370		start := len(dst)
371		dst = ws.appendHidden(dst, true)
372		kept := slices.DeleteFunc(dst[start:], func(p Placement) bool { return slices.Contains(sw.order, p.ID) })
373		dst = dst[:start+len(kept)]
374	}
375	if n == 0 {
376		return dst
377	}
378	gap := max(1, area.W*2/100)
379	boxW := max(min(area.W/5, (area.W*9/10-(n-1)*gap)/n), 1)
380	boxH := max(boxW*area.H/max(area.W, 1), 1)
381	x0 := area.X + (area.W-(n*boxW+(n-1)*gap))/2
382	y0 := area.Y + (area.H-boxH)/2
383	for k, id := range sw.order {
384		s := sw.sizes[id]
385		if s.W <= 0 || s.H <= 0 {
386			s = Rect{W: w.Usable.W, H: w.Usable.H}
387		}
388		z := 1.0
389		if s.W > 0 && s.H > 0 {
390			z = math.Min(1, math.Min(float64(boxW)/float64(s.W), float64(boxH)/float64(s.H)))
391		}
392		rw, rh := int(math.Round(float64(s.W)*z)), int(math.Round(float64(s.H)*z))
393		bx := x0 + k*(boxW+gap)
394		dst = append(dst, Placement{
395			ID:      id,
396			Rect:    Rect{X: bx + (boxW-rw)/2, Y: y0 + (boxH-rh)/2, W: rw, H: rh},
397			Preview: z,
398			Focused: k == sw.at,
399		})
400	}
401	return dst
402}

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