v0.3.0 cf370a0

slide.go RawHistoryFolder

1package core
2
3import (
4	"math"
5	"slices"
6	"time"
7)
8
9// Slides move what is on screen smoothly: a workspace's columns (shift) and
10// the monitor between workspaces (switchOff). Both are pure presentation
11// over the settled state (ViewX, Focus, Active): the layout ends where the
12// state says as soon as a slide stops.
13
14// slidable reports whether a swipe can scroll the columns: only scroll
15// overflow, with the focus on the columns and no fullscreen window.
16func (w *Workspace) slidable() bool {
17	return w.Overflow != OverflowFixed && len(w.Columns) > 0 && w.fullscreen == 0 && !w.onFloat()
18}
19
20func (w *Workspace) stopSlide() { w.shift, w.motion = 0, nil }
21
22// retarget keeps the view where it is on screen when ViewX moved from
23// before during a landing slide: the slide heads for the new view.
24// Without a landing slide the view moves at once, as it always has.
25func (w *Workspace) retarget(before int) {
26	if w.ViewX == before || w.motion == nil {
27		return
28	}
29	w.shift += float64(before - w.ViewX)
30	w.motion = newMotion(viewSpring(w.shift, 0), time.Time{})
31}
32
33func (w *Workspace) shiftPixels() int { return int(math.Round(w.shift)) }
34
35// swipeScale turns touchpad distance into pixels: viewSwipeMovement
36// scrolls one usable width.
37func (w *Workspace) swipeScale() float64 {
38	return float64(w.Usable.W) / viewSwipeMovement
39}
40
41// snapPoints are the sorted views that align a column edge with the usable
42// area, never past the first or last column. Points closer than a
43// snapSpacing of the usable width merge into the first: a swipe step
44// always moves the view visibly.
45func (w *Workspace) snapPoints() []float64 {
46	g := w.gap()
47	minX, maxX := w.Usable.X+g, w.Usable.X+w.Usable.W-g
48	last := len(w.Columns) - 1
49	lo := w.columnX(0) - minX
50	hi := max(w.columnX(last)+w.columnWidth(last)-maxX, lo)
51	views := make([]int, 0, 2*len(w.Columns))
52	for i := range w.Columns {
53		views = append(views, min(max(w.columnX(i)-minX, lo), hi), min(max(w.columnX(i)+w.columnWidth(i)-maxX, lo), hi))
54	}
55	slices.Sort(views)
56	minStep := float64(w.Usable.W) * snapSpacing
57	points := []float64{float64(lo)}
58	for _, v := range views {
59		if float64(v)-points[len(points)-1] >= minStep {
60			points = append(points, float64(v))
61		}
62	}
63	// The last column's end stays a point; a point close before it goes.
64	if n := len(points); float64(hi) != points[n-1] {
65		if n > 1 && float64(hi)-points[n-1] < minStep {
66			points = points[:n-1]
67		}
68		points = append(points, float64(hi))
69	}
70	return points
71}
72
73// snapSpacing is the smallest distance between column snap points, in
74// usable widths.
75const snapSpacing = 0.1
76
77// snapFocus is the column to focus at view: the current one while it stays
78// fully shown at the current view, else the furthest fully shown column
79// toward the swipe (forward is rightward).
80func (w *Workspace) snapFocus(view int, forward bool) (focus int) {
81	g := w.gap()
82	minX, maxX := w.Usable.X+g, w.Usable.X+w.Usable.W-g
83	full := func(i int) bool {
84		x := w.columnX(i) - view
85		return x >= minX && x+w.columnWidth(i) <= maxX
86	}
87	if full(w.Focus) && view == w.ViewX {
88		return w.Focus
89	}
90	focus = -1
91	for i := range w.Columns {
92		if full(i) && (focus < 0 || forward) {
93			focus = i
94		}
95	}
96	if focus < 0 {
97		// Columns wider than the view: the one at the left edge.
98		focus = 0
99		for i := range w.Columns {
100			if w.columnX(i)-view <= minX {
101				focus = i
102			}
103		}
104	}
105	return focus
106}
107
108func (m *Monitor) stopSwitch() { m.switchOff, m.switchMotion, m.switchList = 0, nil, nil }
109
110// framedSwitch keeps transitions involving a sized workspace settled. A
111// monitor-wide animation has no per-workspace crop; it must not expose
112// columns outside their viewport while sliding between different frames.
113func (m *Monitor) framedSwitch() bool {
114	if m.Current().Output != m.Output() {
115		return true
116	}
117	list := m.Workspaces
118	if m.switchList != nil {
119		list = m.switchList
120	}
121	base := indexOf(list, m.Current())
122	if base < 0 {
123		return false
124	}
125	pos := float64(base) + m.switchOff
126	for _, i := range [2]int{int(math.Floor(pos)), int(math.Ceil(pos))} {
127		if i >= 0 && i < len(list) && m.has(list[i]) && list[i].Output != m.Output() {
128			return true
129		}
130	}
131	return false
132}
133
134// slideLayout adds the workspaces a workspace slide shows next to the
135// current one, moved by their distance, to the current layout (moved too).
136// A landing slide places them as the list was when the swipe began
137// (switchList): an empty workspace dropped since shows as background.
138func (m *Monitor) slideLayout(cur []Placement) []Placement {
139	if m.switchOff == 0 || m.shown != nil || m.framedSwitch() {
140		return cur
141	}
142	list := m.Workspaces
143	if m.switchList != nil {
144		list = m.switchList
145	}
146	base := indexOf(list, m.Current())
147	if base < 0 {
148		return cur
149	}
150	h := float64(m.template.Output.H)
151	pos := float64(base) + m.switchOff
152	offset := func(p []Placement, k int) []Placement {
153		dy := int(math.Round((float64(k) - pos) * h))
154		for i := range p {
155			if !p[i].Hidden {
156				p[i].Rect.Y += dy
157			}
158		}
159		return p
160	}
161	cur = offset(cur, base)
162	for _, k := range []int{int(math.Floor(pos)), int(math.Ceil(pos))} {
163		if k == base || k < 0 || k >= len(list) || !m.has(list[k]) {
164			continue
165		}
166		next := list[k].Layout()
167		for i := range next {
168			next[i].Focused = false
169		}
170		// Windows of the neighbor are already listed hidden: replace them.
171		shown := map[WindowID]Placement{}
172		for _, p := range offset(next, k) {
173			shown[p.ID] = p
174		}
175		for i, p := range cur {
176			if v, ok := shown[p.ID]; ok {
177				cur[i] = v
178			}
179		}
180	}
181	return cur
182}

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