v0.2.0 a558f49

selection.go RawHistoryFolder

1package core
2
3import (
4	"errors"
5	"math"
6
7	"github.com/bnema/nefercap/internal/ports"
8)
9
10// Picker selection thresholds, in output-local logical pixels.
11const (
12	// DragThreshold is the pointer travel from the press point at which a
13	// press becomes a drag instead of a click.
14	DragThreshold = 4
15	// MinRegionSize is the smallest accepted dragged region edge.
16	MinRegionSize = 4
17)
18
19// ErrInvalidBounds reports a Picker started with an unusable output or size.
20var ErrInvalidBounds = errors.New("invalid picker bounds")
21
22// PickKind is what a selection covers.
23type PickKind uint8
24
25const (
26	// PickRegion is a dragged rectangle. It is also the default picker kind,
27	// where a click without a drag picks the whole monitor.
28	PickRegion PickKind = iota
29	// PickMonitor is one whole output.
30	PickMonitor
31	// PickWorkspace is the geometry of a workspace whose identity the caller
32	// supplied with SetWorkspace.
33	PickWorkspace
34)
35
36// PickStatus is the picker life cycle.
37type PickStatus uint8
38
39const (
40	// PickIdle is the state before Begin.
41	PickIdle PickStatus = iota
42	PickActive
43	PickAccepted
44	PickCanceled
45)
46
47// PickResult is an accepted selection. Region is explicit output-local
48// logical geometry for drawing: the dragged rectangle, the full output for a
49// monitor, or the workspace rectangle the caller supplied. WorkspaceID is
50// nonzero only for PickWorkspace. Use Target for the capture request.
51type PickResult struct {
52	OutputID    uint32
53	Region      ports.Region
54	Kind        PickKind
55	WorkspaceID uint64
56}
57
58// Target converts the result to a capture target; selectors must use it
59// rather than map results themselves. A monitor is the zero region of its
60// output. A region carries its geometry. A workspace carries only its exact
61// Source-local WorkspaceID: the compositor supplies live geometry at capture, so
62// the Region snapshot in PickResult is for drawing only and is not cached in
63// the target. A workspace is never converted to a monitor. Sizes are logical:
64// the caller decides physical resolution and rounding.
65func (r PickResult) Target() ports.Target {
66	switch r.Kind {
67	case PickMonitor:
68		return ports.Target{OutputID: r.OutputID}
69	case PickWorkspace:
70		return ports.Target{OutputID: r.OutputID, WorkspaceID: r.WorkspaceID}
71	}
72	return ports.Target{OutputID: r.OutputID, Region: r.Region}
73}
74
75// Picker is the pure selection state for one output. It has no framework
76// types, performs no allocation per event and is not safe for concurrent use.
77// Coordinates are output-local logical pixels. Mouse methods model the left
78// button only; callers filter other buttons. Every mutating method returns
79// whether visible state changed, so callers redraw only then.
80//
81// Interaction: dragging past DragThreshold and releasing accepts a region;
82// a click accepts the whole monitor (or, after SelectWorkspace, the
83// workspace); Confirm accepts the kind chosen by SelectMonitor or
84// SelectWorkspace; Cancel abandons the pick.
85type Picker struct {
86	status   PickStatus
87	kind     PickKind
88	outputID uint32
89	width    int
90	height   int
91
92	px, py float64 // clamped pointer
93	ax, ay float64 // clamped press point
94	rect   ports.Region
95	pressed,
96	dragging bool
97
98	wsID   uint64
99	wsRect ports.Region
100
101	result PickResult
102}
103
104// Begin (re)starts the picker on an output of the given logical size,
105// clearing all state including any workspace.
106func (p *Picker) Begin(outputID uint32, width, height int) error {
107	if outputID == 0 || width < 1 || height < 1 || width > ports.MaxDimension || height > ports.MaxDimension {
108		*p = Picker{}
109		return ErrInvalidBounds
110	}
111	*p = Picker{status: PickActive, outputID: outputID, width: width, height: height}
112	return nil
113}
114
115// Status returns the life-cycle state.
116func (p *Picker) Status() PickStatus { return p.status }
117
118// Kind returns the chosen kind (PickRegion by default).
119func (p *Picker) Kind() PickKind { return p.kind }
120
121// Dragging reports whether a press has passed DragThreshold.
122func (p *Picker) Dragging() bool { return p.dragging }
123
124// Rect returns the normalized live drag rectangle; ok is false when not
125// dragging.
126func (p *Picker) Rect() (r ports.Region, ok bool) { return p.rect, p.dragging }
127
128// Bounds returns the output logical size.
129func (p *Picker) Bounds() (width, height int) { return p.width, p.height }
130
131// Workspace returns the supplied workspace identity and its rectangle
132// clipped to the output; ok is false when none is available.
133func (p *Picker) Workspace() (id uint64, r ports.Region, ok bool) {
134	return p.wsID, p.wsRect, p.wsID != 0
135}
136
137// Result returns the accepted selection; ok is false unless Status is
138// PickAccepted.
139func (p *Picker) Result() (PickResult, bool) {
140	return p.result, p.status == PickAccepted
141}
142
143// SetWorkspace supplies the current workspace identity and rectangle in
144// output-local logical coordinates. Only a nonzero id with a rectangle that
145// overlaps the output is eligible; the rectangle is clipped to the output.
146// Anything else clears the workspace (and leaves PickWorkspace for
147// PickRegion). It never invents an identity.
148func (p *Picker) SetWorkspace(id uint64, r ports.Region) bool {
149	if p.status != PickActive {
150		return false
151	}
152	clipped, ok := p.clip(r)
153	if id == 0 || !ok {
154		id, clipped = 0, ports.Region{}
155	}
156	if id == p.wsID && clipped == p.wsRect {
157		return false
158	}
159	p.wsID, p.wsRect = id, clipped
160	if id == 0 && p.kind == PickWorkspace {
161		p.kind = PickRegion
162	}
163	return true
164}
165
166// SelectWorkspace makes Confirm and click pick the supplied workspace. It
167// does nothing when no eligible workspace was supplied.
168func (p *Picker) SelectWorkspace() bool { return p.setKind(PickWorkspace) }
169
170// SelectMonitor makes Confirm pick the whole monitor.
171func (p *Picker) SelectMonitor() bool { return p.setKind(PickMonitor) }
172
173// SelectRegion returns to the default kind.
174func (p *Picker) SelectRegion() bool { return p.setKind(PickRegion) }
175
176func (p *Picker) setKind(k PickKind) bool {
177	if p.status != PickActive || p.pressed || p.kind == k || (k == PickWorkspace && p.wsID == 0) {
178		return false
179	}
180	p.kind = k
181	return true
182}
183
184// MouseDown starts a press at (x, y).
185func (p *Picker) MouseDown(x, y float64) bool {
186	if p.status != PickActive || p.pressed {
187		return false
188	}
189	p.setPointer(x, y)
190	p.pressed, p.dragging = true, false
191	p.ax, p.ay = p.px, p.py
192	p.rect = ports.Region{}
193	return true
194}
195
196// MouseMove moves the pointer, growing the drag once past DragThreshold.
197func (p *Picker) MouseMove(x, y float64) bool {
198	if p.status != PickActive {
199		return false
200	}
201	changed := p.setPointer(x, y)
202	if !p.pressed {
203		return changed
204	}
205	if !p.dragging {
206		dx, dy := p.px-p.ax, p.py-p.ay
207		if dx*dx+dy*dy < DragThreshold*DragThreshold {
208			return changed
209		}
210		p.dragging = true
211		changed = true
212	}
213	if r := p.dragRect(); r != p.rect {
214		p.rect = r
215		changed = true
216	}
217	return changed
218}
219
220// MouseUp ends the press. A drag accepts its region when both edges reach
221// MinRegionSize, otherwise the drag is discarded and picking continues. A
222// click accepts the workspace when PickWorkspace is chosen, else the monitor.
223func (p *Picker) MouseUp(x, y float64) bool {
224	if p.status != PickActive || !p.pressed {
225		return false
226	}
227	p.setPointer(x, y)
228	p.pressed = false
229	if !p.dragging {
230		if p.kind == PickWorkspace && p.wsID != 0 {
231			p.accept(PickWorkspace)
232		} else {
233			p.accept(PickMonitor)
234		}
235		return true
236	}
237	p.dragging = false
238	r := p.dragRect()
239	p.rect = ports.Region{}
240	if r.Width < MinRegionSize || r.Height < MinRegionSize {
241		return true
242	}
243	p.status = PickAccepted
244	p.result = PickResult{OutputID: p.outputID, Region: r, Kind: PickRegion}
245	return true
246}
247
248// AbandonPress drops an unfinished press and any drag preview, for example
249// when the pointer leaves the surface or input focus is lost, so a button
250// release that is never delivered cannot leave the picker stuck. The chosen
251// kind and workspace are kept and the status stays PickActive. It does
252// nothing after acceptance or cancellation. It reports whether state changed.
253func (p *Picker) AbandonPress() bool {
254	if p.status != PickActive || !(p.pressed || p.dragging || p.rect != (ports.Region{})) {
255		return false
256	}
257	p.pressed, p.dragging = false, false
258	p.rect = ports.Region{}
259	return true
260}
261
262// Confirm accepts the chosen monitor or workspace. It does nothing for the
263// default region kind, whose selection is made by the mouse.
264func (p *Picker) Confirm() bool {
265	if p.status != PickActive || p.pressed {
266		return false
267	}
268	switch {
269	case p.kind == PickMonitor:
270		p.accept(PickMonitor)
271	case p.kind == PickWorkspace && p.wsID != 0:
272		p.accept(PickWorkspace)
273	default:
274		return false
275	}
276	return true
277}
278
279// Cancel abandons the pick (Escape).
280func (p *Picker) Cancel() bool {
281	if p.status != PickActive {
282		return false
283	}
284	p.status = PickCanceled
285	p.pressed, p.dragging = false, false
286	p.rect = ports.Region{}
287	return true
288}
289
290func (p *Picker) accept(k PickKind) {
291	p.status = PickAccepted
292	switch k {
293	case PickWorkspace:
294		p.result = PickResult{OutputID: p.outputID, Region: p.wsRect, Kind: PickWorkspace, WorkspaceID: p.wsID}
295	default:
296		p.result = PickResult{OutputID: p.outputID, Region: ports.Region{Width: p.width, Height: p.height}, Kind: PickMonitor}
297	}
298}
299
300// setPointer clamps into the output and reports movement. NaN is ignored;
301// +Inf and -Inf clamp to the output edges like any out-of-range value.
302func (p *Picker) setPointer(x, y float64) bool {
303	if math.IsNaN(x) || math.IsNaN(y) {
304		return false
305	}
306	x = math.Min(math.Max(x, 0), float64(p.width))
307	y = math.Min(math.Max(y, 0), float64(p.height))
308	moved := x != p.px || y != p.py
309	p.px, p.py = x, y
310	return moved
311}
312
313// dragRect is the normalized rectangle between press and pointer, rounded to
314// whole logical pixels.
315func (p *Picker) dragRect() ports.Region {
316	x0, x1 := math.Round(math.Min(p.ax, p.px)), math.Round(math.Max(p.ax, p.px))
317	y0, y1 := math.Round(math.Min(p.ay, p.py)), math.Round(math.Max(p.ay, p.py))
318	return ports.Region{X: int(x0), Y: int(y0), Width: int(x1 - x0), Height: int(y1 - y0)}
319}
320
321// clip intersects r with the output; ok is false when empty or invalid.
322func (p *Picker) clip(r ports.Region) (ports.Region, bool) {
323	x0, y0 := max(int64(r.X), 0), max(int64(r.Y), 0)
324	x1 := min(int64(r.X)+int64(r.Width), int64(p.width))
325	y1 := min(int64(r.Y)+int64(r.Height), int64(p.height))
326	if r.Width < 1 || r.Height < 1 || x1 <= x0 || y1 <= y0 {
327		return ports.Region{}, false
328	}
329	return ports.Region{X: int(x0), Y: int(y0), Width: int(x1 - x0), Height: int(y1 - y0)}, true
330}

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 2

Tags 9

↑↓ moveenter openesc close