main f2d166b

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 (view), its
10// stash (stashView) and the monitor between workspaces (switchView). All
11// are pure presentation over the settled state (View, Focus, stashAt,
12// Active): the layout ends where the state says as soon as a slide stops.
13
14// slide is a view offset that a swipe sets while the fingers follow it, and
15// a spring then takes to 0 (off: pixels for the columns, windows for the
16// stash, workspaces for the monitor). Pure presentation over settled state.
17type slide struct {
18	off    float64
19	motion motion
20}
21
22// stop lands the slide where its state says.
23func (s *slide) stop() { *s = slide{} }
24
25// step moves a running spring to now and stops it once it settles.
26func (s *slide) step(now time.Time) {
27	if !s.motion.on {
28		return
29	}
30	v, done := s.motion.at(now)
31	s.off = v
32	if done {
33		s.stop()
34	}
35}
36
37// busy reports whether the slide is off its state or its spring runs.
38func (s *slide) busy() bool { return s.motion.on || s.off != 0 }
39
40// slidable reports whether a swipe can scroll the columns: scroll or cascade
41// overflow, with the focus on the columns and no fullscreen window.
42func (w *Workspace) slidable() bool {
43	return w.Overflow != OverflowFixed && len(w.Columns) > 0 && w.fullscreen == 0 && !w.onFloat()
44}
45
46// retarget keeps the view where it is on screen when View moved from
47// before during a landing slide: the slide heads for the new view.
48// Without a landing slide the view moves at once, as it always has.
49func (w *Workspace) retarget(before int) {
50	if w.View == before || !w.view.motion.on {
51		return
52	}
53	w.view.off += float64(before - w.View)
54	w.view.motion = newMotion(viewSpring(w.view.off, w.view.motion.velocity()), time.Time{}, w.view.motion.slow)
55}
56
57func (w *Workspace) shiftPixels() int { return int(math.Round(w.view.off)) }
58
59// switchSpring is the workspace slide's spring from off: it snaps once it
60// is under half a logical pixel of the output span along the switch axis
61// from its target (slideLayout rounds the offset times that span).
62func (m *Monitor) switchSpring(off, velocity float64) spring {
63	s := workspaceSpring(off, velocity)
64	if h := m.switchAxis.span(m.template.Output); h > 0 {
65		s.Snap = pixelSnap / float64(h)
66	}
67	return s
68}
69
70// swipeScale turns touchpad distance into pixels: viewSwipeMovement
71// scrolls one usable width.
72func (w *Workspace) swipeScale() float64 {
73	return float64(w.policy().content.span(w.Usable)) / viewSwipeMovement
74}
75
76// snapPoints are the sorted views that align a column edge with the usable
77// area, never past the first or last column. Points closer than a
78// snapSpacing of the usable width merge into the first: a swipe step
79// always moves the view visibly.
80func (w *Workspace) snapPoints() []float64 {
81	if w.policy().wraps {
82		return w.cascadePoints()
83	}
84	g := w.gap()
85	minX, maxX := w.Usable.X+g, w.Usable.X+w.Usable.W-g
86	last := len(w.Columns) - 1
87	xs, widths := w.columnEdges()
88	lo := xs[0] - minX
89	hi := max(xs[last]+widths[last]-maxX, lo)
90	views := make([]int, 0, 2*len(w.Columns))
91	for i := range w.Columns {
92		views = append(views, min(max(xs[i]-minX, lo), hi), min(max(xs[i]+widths[i]-maxX, lo), hi))
93	}
94	slices.Sort(views)
95	minStep := float64(w.Usable.W) * snapSpacing
96	points := []float64{float64(lo)}
97	for _, v := range views {
98		if float64(v)-points[len(points)-1] >= minStep {
99			points = append(points, float64(v))
100		}
101	}
102	// The last column's end stays a point; a point close before it goes.
103	if n := len(points); float64(hi) != points[n-1] {
104		if n > 1 && float64(hi)-points[n-1] < minStep {
105			points = points[:n-1]
106		}
107		points = append(points, float64(hi))
108	}
109	return points
110}
111
112// snapSpacing is the smallest distance between column snap points, in
113// usable widths.
114const snapSpacing = 0.1
115
116// snapFocus is the column to focus at view: the current one while it stays
117// fully shown at the current view, else the furthest fully shown column
118// toward the swipe (forward is rightward).
119func (w *Workspace) snapFocus(view int, forward bool) (focus int) {
120	if w.policy().wraps {
121		return w.cascadeFocus(view)
122	}
123	g := w.gap()
124	minX, maxX := w.Usable.X+g, w.Usable.X+w.Usable.W-g
125	xs, widths := w.columnEdges()
126	full := func(i int) bool {
127		x := xs[i] - view
128		return x >= minX && x+widths[i] <= maxX
129	}
130	if full(w.Focus) && view == w.View {
131		return w.Focus
132	}
133	focus = -1
134	for i := range w.Columns {
135		if full(i) && (focus < 0 || forward) {
136			focus = i
137		}
138	}
139	if focus < 0 {
140		// Columns wider than the view: the one at the left edge.
141		focus = 0
142		for i := range w.Columns {
143			if xs[i]-view <= minX {
144				focus = i
145			}
146		}
147	}
148	return focus
149}
150
151// stopSwitch lands the workspace slide and forgets the list it measured in.
152func (m *Monitor) stopSwitch() { m.switchView.stop(); m.switchList = nil }
153
154// framedSwitch keeps transitions involving a sized workspace settled. A
155// monitor-wide animation has no per-workspace crop; it must not expose
156// columns outside their viewport while sliding between different frames.
157func (m *Monitor) framedSwitch() bool {
158	if m.Current().Output != m.Output() {
159		return true
160	}
161	list := m.Workspaces
162	if m.switchList != nil {
163		list = m.switchList
164	}
165	base := indexOf(list, m.Current())
166	if base < 0 {
167		return false
168	}
169	pos := float64(base) + m.switchView.off
170	for _, i := range [2]int{int(math.Floor(pos)), int(math.Ceil(pos))} {
171		if i >= 0 && i < len(list) && m.has(list[i]) && list[i].Output != m.Output() {
172			return true
173		}
174	}
175	return false
176}
177
178// slideLayout adds the workspaces a workspace slide shows next to the
179// current one, moved by their distance, to the current layout (moved too).
180// A landing slide places them as the list was when the swipe began
181// (switchList): an empty workspace dropped since shows as background.
182func (m *Monitor) slideLayout(cur []Placement) []Placement {
183	if m.switchView.off == 0 || m.shown != nil || m.framedSwitch() {
184		return cur
185	}
186	list := m.Workspaces
187	if m.switchList != nil {
188		list = m.switchList
189	}
190	base := indexOf(list, m.Current())
191	if base < 0 {
192		return cur
193	}
194	h := float64(m.switchAxis.span(m.template.Output))
195	pos := float64(base) + m.switchView.off
196	offset := func(p []Placement, k int) []Placement {
197		d := int(math.Round((float64(k) - pos) * h))
198		for i := range p {
199			if !p[i].Hidden {
200				m.switchAxis.offset(&p[i].Rect, d)
201			}
202		}
203		return p
204	}
205	cur = offset(cur, base)
206	for _, k := range []int{int(math.Floor(pos)), int(math.Ceil(pos))} {
207		if k == base || k < 0 || k >= len(list) || !m.has(list[k]) {
208			continue
209		}
210		next := list[k].layoutInto(m.slideBuf)
211		m.slideBuf = next
212		for i := range next {
213			next[i].Focused = false
214		}
215		// Windows of the neighbor are already listed hidden: replace them.
216		if m.slideShown == nil {
217			m.slideShown = map[WindowID]Placement{}
218		}
219		shown := m.slideShown
220		clear(shown)
221		for _, p := range offset(next, k) {
222			shown[p.ID] = p
223		}
224		for i, p := range cur {
225			if v, ok := shown[p.ID]; ok {
226				cur[i] = v
227			}
228		}
229	}
230	return cur
231}

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