pulse.go RawHistoryFolder

1package core
2
3import (
4	"math"
5	"time"
6
7	"github.com/bnema/neferwl/internal/ports"
8)
9
10// The focus pulse is the pulse animation of the focus indicator
11// (focus.animation): it briefly shows the indicator's effect (focus.effect, a
12// light screen blend visible on black backgrounds too) on a window that just
13// got the keyboard focus. Only the last focus counts: it must hold for
14// pulseSettle before the pulse starts, a focus change stops a running one, and
15// the window that pulsed last does not pulse again within pulseCooldown
16// (switching back after another window held the focus is a new change).
17// Fast switching therefore shows nothing. Fullscreen windows, a window alone
18// in the usable area (e.g. a maximized column), overview previews, a window focused as
19// it maps and a protected session never pulse.
20
21const (
22	pulseSettle   = 150 * time.Millisecond
23	pulseRise     = 90 * time.Millisecond
24	pulseFall     = 230 * time.Millisecond
25	pulseCooldown = time.Second
26	// pulseRecheck is how often a pulse waiting for a slide to land checks again.
27	pulseRecheck = pulseSettle / 3
28)
29
30// focusPulse is the pulse state. target is the window focus last seen and
31// since when; id is the window pulsing from start (0: none). last and lastAt
32// are the previous pulse, for the cooldown. drawable is whether the last
33// scene could show a pulse on target.
34type focusPulse struct {
35	target   WindowID
36	since    time.Time
37	id       WindowID
38	start    time.Time
39	last     WindowID
40	lastAt   time.Time
41	drawable bool
42	// value is the effect of the last sample, kept for scenes published
43	// between the focused output's own flips.
44	value     float64
45	timerC    <-chan time.Time
46	timerStop func() bool
47}
48
49func (c *Core) pulseOn() bool {
50	return c.animOn() && c.cfg.Focus.Animation == ports.FocusAnimationPulse
51}
52
53// pulseFocus follows the window focus. held is false while something else
54// (a popup grab, a layer, a lock surface) has the keyboard: the running pulse
55// stops, and the window getting it back is not a new focus. A new window
56// stops the running pulse and waits pulseSettle before one starts. With the
57// pulse off the focus is still followed, so turning it on pulses nothing.
58func (c *Core) pulseFocus(window WindowID, held bool) {
59	p := &c.pulse
60	if !held || !c.pulseOn() {
61		p.id = 0
62		c.stopPulseTimer()
63		if held {
64			p.target = window
65		}
66		return
67	}
68	if window == p.target {
69		return
70	}
71	p.target, p.id = window, 0
72	c.stopPulseTimer()
73	if window == 0 {
74		return
75	}
76	p.since = c.now()
77	if c.justMapped(window) {
78		return
79	}
80	p.timerC, p.timerStop = newTimer(c.opts.Clock, pulseSettle)
81}
82
83// justMapped reports whether the window got the focus as it mapped: opening
84// an app is not a focus change, and its first frames may still be on their
85// way. Its focus counts from the next change.
86func (c *Core) justMapped(id WindowID) bool {
87	p := &c.pulse
88	return p.target == id && p.since.Sub(c.windows.lookup(id).mappedAt) < pulseSettle
89}
90
91// pulseTick runs when the settle timer fired. It reports whether a pulse
92// started.
93func (c *Core) pulseTick() bool {
94	p := &c.pulse
95	p.timerC, p.timerStop = nil, nil
96	if p.target == 0 || !c.pulseOn() || !p.drawable || c.justMapped(p.target) {
97		return false
98	}
99	now := c.now()
100	if wait := pulseSettle - now.Sub(p.since); wait > 0 {
101		p.timerC, p.timerStop = newTimer(c.opts.Clock, wait)
102		return false
103	}
104	// A swipe or its landing slide still shows the neighbours: decide on
105	// the settled layout, or a window left alone would start a pulse cut
106	// short.
107	if !c.cur().settled() {
108		p.timerC, p.timerStop = newTimer(c.opts.Clock, pulseRecheck)
109		return false
110	}
111	if p.target == p.last && now.Sub(p.lastAt) < pulseCooldown {
112		return false
113	}
114	p.id, p.start, p.last, p.lastAt = p.target, now, p.target, now
115	return true
116}
117
118// advancePulse is the effect of the running pulse at now: a quick sine rise
119// to focus.strength, then a smooth fall. It ends the pulse once over.
120func (c *Core) advancePulse(now time.Time) float64 {
121	p := &c.pulse
122	if p.id == 0 {
123		return 0
124	}
125	peak := c.cfg.Focus.Strength
126	t := now.Sub(p.start)
127	switch {
128	case t < 0:
129		return 0
130	case t < pulseRise:
131		return peak * math.Sin(float64(t)/float64(pulseRise)*math.Pi/2)
132	case t < pulseRise+pulseFall:
133		x := 1 - float64(t-pulseRise)/float64(pulseFall)
134		return peak * x * x * (3 - 2*x)
135	}
136	p.id = 0
137	return 0
138}
139
140// settled reports whether no swipe or slide moves the monitor's layout
141// (screen.settled adds the rect motions).
142func (m *Monitor) settled() bool {
143	if m.switchView.busy() {
144		return false
145	}
146	for w := range m.all() {
147		if w.view.busy() || w.stashView.busy() {
148			return false
149		}
150	}
151	return true
152}
153
154// visibleCount is the number of windows of layout seen in v.
155func visibleCount(layout []Placement, v viewport) int {
156	n := 0
157	for _, p := range layout {
158		if p.Preview == 0 && v.shows(p) {
159			n++
160		}
161	}
162	return n
163}
164
165// pulsing reports whether a pulse runs on the screen.
166func (c *Core) pulsing(sc *screen) bool {
167	return c.pulse.id != 0 && sc == c.cur()
168}
169
170func (c *Core) stopPulseTimer() {
171	p := &c.pulse
172	if p.timerStop != nil {
173		p.timerStop()
174	}
175	p.timerC, p.timerStop = nil, nil
176}
177
178// stopPulse ends the running pulse and its timer (security change). The
179// focus is kept: the window focused again after an unlock does not pulse.
180func (c *Core) stopPulse() {
181	c.stopPulseTimer()
182	c.pulse.id, c.pulse.drawable = 0, false
183}

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