Repository navigation
Expand file tree
/
Copy pathinput.go
More file actions
312 lines (286 loc) · 11.2 KB
/
Copy pathinput.go
File metadata and controls
312 lines (286 loc) · 11.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
package figure
import (
"math"
"github.com/timzifer/figure/ir"
)
// Input turns a surface's raw pointer input into chart interaction.
//
// [Live] takes deliberate instructions — hover here, zoom about there by this
// much, pan by that. A surface reports something rawer: a button went down, the
// pointer moved, the wheel turned by ninety-six of whatever units this platform
// counts in. The translation between the two is a small state machine — is this
// move a hover or a drag, was that release a click or the end of a pan — and it
// is the same state machine on every surface.
//
// So it lives here, once, rather than being written again in every backend.
// [Live.Bind] is this driving a DOM element; a native window drives it from its
// own event loop; a test drives it directly. A backend consumes IR and must not
// know what a panel or a scale is, which is why none of this is in one.
//
// in := live.Input()
// // from the surface's event loop:
// in.Down(x, y)
// in.Move(x, y) // pans, because a button is down
// in.Up(x, y) // clicks, because the pointer barely moved
// in.Wheel(x, y, deltaY)
//
// An Input is not safe for concurrent use, and neither is the Live behind it.
type Input struct {
l *Live
down bool
dragging bool
downX float64
downY float64
lastX float64
lastY float64
slop float64
drag Drag
// bar is the colourbar a drag started on, and barTo the last point of it
// the pointer was over. A drag that begins on a colourbar is a range along
// it whatever [Input.Drag] says, because a bar cannot be panned and a
// rectangle dragged out over one means the values between its ends.
onBar bool
bar, barTo Hit
}
// Drag is what dragging the pointer across the chart does.
type Drag uint8
// The drag modes.
const (
// DragPans moves the view under the pointer, which is what a drag has
// always done and what a reader of a map expects.
DragPans Drag = iota
// DragSelects drags out a rectangle and fires [Select] with the rows
// under it on release. The view does not move.
DragSelects
// DragZooms drags out a rectangle and zooms to it on release, which is
// the other thing a rubber band conventionally means.
DragZooms
)
// String names the mode, for tests and error messages.
func (d Drag) String() string {
switch d {
case DragPans:
return "pan"
case DragSelects:
return "select"
case DragZooms:
return "zoom"
}
return "unknown"
}
// Drag sets what a drag does and returns i, so the call can be chained onto
// [Live.Input].
//
// It is a mode rather than a modifier key because a modifier is a fact about a
// keyboard and this package has never seen one: a browser reports shift on its
// own events, a window reports it on its own, and a touch screen has none at
// all. A surface that wants shift-to-select reads its own event and sets the
// mode; a surface that wants a toolbar button sets it from the button. Either
// way the state machine is the same one.
//
// Changing it mid-drag takes effect on the next press, so a mode switched
// under a held button does not turn half a pan into half a selection.
//
// A drag that *starts on a colourbar* ignores this and selects a range of
// values along the bar, whatever the mode says. There is no view to pan on a
// bar and no rectangle to zoom to, so a drag over one has exactly one sensible
// reading — see [Live.Select] and [interact.Colorbar].
func (i *Input) Drag(d Drag) *Input { i.drag = d; return i }
// Dragged reports the rectangle a rubber-band drag currently covers, and
// whether there is one. A surface draws it as feedback; [Input.Dragging] is
// the same question without the geometry.
//
// It is empty in [DragPans], where a drag moves the chart rather than marking
// out part of it.
func (i *Input) Dragged() (ir.Rect, bool) {
if !i.dragging {
return ir.Rect{}, false
}
if i.onBar {
// Along the bar, across all of it: a range on a colourbar is an
// interval of values, and how far sideways the pointer wandered while
// choosing it means nothing.
return ir.Rect{
Min: ir.Point{X: i.bar.Area.Min.X, Y: float32(min(i.downY, i.lastY))},
Max: ir.Point{X: i.bar.Area.Max.X, Y: float32(max(i.downY, i.lastY))},
}, true
}
if i.drag == DragPans {
return ir.Rect{}, false
}
return ir.Rect{
Min: ir.Point{X: float32(min(i.downX, i.lastX)), Y: float32(min(i.downY, i.lastY))},
Max: ir.Point{X: float32(max(i.downX, i.lastX)), Y: float32(max(i.downY, i.lastY))},
}, true
}
// DefaultClickSlop is how far the pointer may travel between press and release
// and still count as a click rather than a drag, in device-independent pixels.
//
// It is not zero because a pointer never is: a hand on a mouse moves a pixel or
// two during a click, and a finger on a trackpad more. Three pixels is the
// figure the desktop toolkits settled on, and it is well below the distance a
// deliberate pan covers.
const DefaultClickSlop = 3
// Input returns a driver for this chart's surface input.
//
// Each call returns a fresh driver with no button held. A surface wants one,
// made once and kept for as long as the Live is open.
func (l *Live) Input() *Input { return &Input{l: l, slop: DefaultClickSlop} }
// Live returns the chart this input drives.
func (i *Input) Live() *Live { return i.l }
// ClickSlop sets how far the pointer may move between press and release and
// still count as a click. It returns i so the call can be chained.
func (i *Input) ClickSlop(px float64) *Input {
if px >= 0 {
i.slop = px
}
return i
}
// Move reports the pointer at a device position.
//
// With no button held it hovers, which fires [Hover] and reports the mark under
// the pointer. With a button held it pans, which drags the data under the
// pointer and redraws — so the value the reader grabbed stays under their
// finger, which is the whole reason a drag pans in the direction it does.
func (i *Input) Move(x, y float64) error {
if i.down {
i.dragging = i.dragging || i.beyondSlop(x, y)
dx, dy := x-i.lastX, y-i.lastY
i.lastX, i.lastY = x, y
if !i.dragging {
// Still inside the slop: this is the wobble of a click, not a pan.
return nil
}
if i.onBar {
// A drag along a colourbar selects a range of values. It moves
// nothing: there is no view to pan on a bar, and the rectangle
// being dragged out is what it means.
if h, ok := i.l.Index().At(ir.Point{X: float32(x), Y: float32(y)}, 0); ok && h.Kind == Colorbar {
i.barTo = h
}
return nil
}
if i.drag != DragPans {
// A rubber band moves nothing while it is being dragged out. The
// rectangle is [Input.Dragged]; what to do with it before release
// — draw it, or not — is the surface's.
return nil
}
return i.l.PanBy(dx, dy)
}
i.lastX, i.lastY = x, y
i.l.Move(x, y)
// A hover does not change the chart, so Live.Move does not draw. A chart
// with an overlay is the exception: the handler that just ran is where a
// crosshair's position or a tooltip's text is set, and nothing else is
// going to repaint. A frame identical to the last is still not painted, so
// a chart without an overlay pays nothing for this line.
if i.l.CurrentOverlay() != nil {
return i.l.Draw()
}
return nil
}
// Down reports a button pressed at a device position. It starts a drag, which
// becomes a pan once the pointer has moved past the click slop.
func (i *Input) Down(x, y float64) error {
i.down, i.dragging = true, false
i.downX, i.downY = x, y
i.lastX, i.lastY = x, y
i.onBar, i.bar, i.barTo = false, Hit{}, Hit{}
if h, ok := i.l.Index().At(ir.Point{X: float32(x), Y: float32(y)}, 0); ok && h.Kind == Colorbar {
i.onBar, i.bar, i.barTo = true, h, h
}
return nil
}
// Up reports the button released at a device position.
//
// A release that never moved past the click slop is a click, and fires [Click];
// one that did is the end of a pan and fires nothing, because every step of it
// has already fired [Pan]. That is what keeps a dragged chart from also
// selecting whatever the pointer happened to land on.
//
// A release with no press behind it fires nothing either. A surface has more
// ways to lose a press than to report one — a double click consumed by
// [Input.DoubleClick], a press that started outside the chart, a window that
// took the pointer away — and inventing a click for each of them would put a
// tooltip on screen every time a reader reset the view.
func (i *Input) Up(x, y float64) error {
if !i.down {
i.lastX, i.lastY = x, y
return nil
}
// The release position is part of the drag: it is the far corner of a
// rubber band, so it is recorded before the band is read and not after.
dragged := i.dragging || i.beyondSlop(x, y)
i.dragging = dragged
i.lastX, i.lastY = x, y
mode := i.drag
band, hasBand := i.Dragged()
onBar, from, to := i.onBar, i.bar, i.barTo
i.down, i.dragging = false, false
i.onBar, i.bar, i.barTo = false, Hit{}, Hit{}
if !dragged {
i.l.Click(x, y)
return nil
}
if onBar {
i.l.selectRange(from, to, band)
return nil
}
// A pan has already fired on every step of itself and has nothing left to
// report. A rubber band has done nothing at all until now: this is where
// it means something.
if !hasBand {
return nil
}
switch mode {
case DragSelects:
i.l.Select(band)
case DragZooms:
return i.l.ZoomTo(band)
}
return nil
}
// Leave reports the pointer leaving the surface. It cancels any drag in
// progress and fires [Leave], so that a tooltip opened on a hover closes.
func (i *Input) Leave() error {
i.down, i.dragging = false, false
i.l.Leave()
return nil
}
// DoubleClick resets the view, releasing every zoom and pan, and redraws. It is
// the one control a reader looks for first.
func (i *Input) DoubleClick() error {
i.down, i.dragging = false, false
return i.l.Autoscale()
}
// Wheel zooms about a device position by a raw scroll delta, and redraws.
//
// The delta is in the browser's pixel convention, which is the one every
// platform can be converted into: positive scrolls the content away from the
// reader and zooms out, and one notch of a mouse wheel is about a hundred.
// [WheelFactor] is the curve it goes through.
func (i *Input) Wheel(x, y, delta float64) error {
i.lastX, i.lastY = x, y
return i.l.Wheel(x, y, WheelFactor(delta))
}
// Resize reports the surface's new size and redraws. See [Live.Resize].
func (i *Input) Resize(w, h int) error { return i.l.Resize(w, h) }
// Rescale reports the surface's new device pixel ratio and redraws. See
// [Live.Rescale].
func (i *Input) Rescale(dpr float64) error { return i.l.Rescale(dpr) }
// Dragging reports whether a drag is in progress — a button is held and the
// pointer has moved past the click slop. A surface uses it to decide what
// cursor to show.
func (i *Input) Dragging() bool { return i.dragging }
func (i *Input) beyondSlop(x, y float64) bool {
return math.Abs(x-i.downX) > i.slop || math.Abs(y-i.downY) > i.slop
}
// WheelFactor turns a raw scroll delta into a zoom factor.
//
// The exponential keeps a fast scroll from inverting: any delta maps into
// (0, ∞) and never through zero, so holding the wheel down zooms smoothly
// rather than jumping. The rate is chosen so that one notch on a mouse — a
// hundred units in the browser's pixel mode — is about ten percent, which is
// the step a reader expects from a map.
func WheelFactor(delta float64) float64 { return math.Exp(delta / 1000) }