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}