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// Confirm accepts the chosen monitor or workspace. It does nothing for the
249// default region kind, whose selection is made by the mouse.
250func (p *Picker) Confirm() bool {
251 if p.status != PickActive || p.pressed {
252 return false
253 }
254 switch {
255 case p.kind == PickMonitor:
256 p.accept(PickMonitor)
257 case p.kind == PickWorkspace && p.wsID != 0:
258 p.accept(PickWorkspace)
259 default:
260 return false
261 }
262 return true
263}
264
265// Cancel abandons the pick (Escape).
266func (p *Picker) Cancel() bool {
267 if p.status != PickActive {
268 return false
269 }
270 p.status = PickCanceled
271 p.pressed, p.dragging = false, false
272 p.rect = ports.Region{}
273 return true
274}
275
276func (p *Picker) accept(k PickKind) {
277 p.status = PickAccepted
278 switch k {
279 case PickWorkspace:
280 p.result = PickResult{OutputID: p.outputID, Region: p.wsRect, Kind: PickWorkspace, WorkspaceID: p.wsID}
281 default:
282 p.result = PickResult{OutputID: p.outputID, Region: ports.Region{Width: p.width, Height: p.height}, Kind: PickMonitor}
283 }
284}
285
286// setPointer clamps into the output and reports movement. NaN is ignored;
287// +Inf and -Inf clamp to the output edges like any out-of-range value.
288func (p *Picker) setPointer(x, y float64) bool {
289 if math.IsNaN(x) || math.IsNaN(y) {
290 return false
291 }
292 x = math.Min(math.Max(x, 0), float64(p.width))
293 y = math.Min(math.Max(y, 0), float64(p.height))
294 moved := x != p.px || y != p.py
295 p.px, p.py = x, y
296 return moved
297}
298
299// dragRect is the normalized rectangle between press and pointer, rounded to
300// whole logical pixels.
301func (p *Picker) dragRect() ports.Region {
302 x0, x1 := math.Round(math.Min(p.ax, p.px)), math.Round(math.Max(p.ax, p.px))
303 y0, y1 := math.Round(math.Min(p.ay, p.py)), math.Round(math.Max(p.ay, p.py))
304 return ports.Region{X: int(x0), Y: int(y0), Width: int(x1 - x0), Height: int(y1 - y0)}
305}
306
307// clip intersects r with the output; ok is false when empty or invalid.
308func (p *Picker) clip(r ports.Region) (ports.Region, bool) {
309 x0, y0 := max(int64(r.X), 0), max(int64(r.Y), 0)
310 x1 := min(int64(r.X)+int64(r.Width), int64(p.width))
311 y1 := min(int64(r.Y)+int64(r.Height), int64(p.height))
312 if r.Width < 1 || r.Height < 1 || x1 <= x0 || y1 <= y0 {
313 return ports.Region{}, false
314 }
315 return ports.Region{X: int(x0), Y: int(y0), Width: int(x1 - x0), Height: int(y1 - y0)}, true
316}