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
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
type id = Widget.id
type capture = Free | Held of id | Elsewhere
type t = {
input : Widget.input;
theme : Theme.t;
was_down : bool;
press : bool;
was_rdown : bool;
rpress : bool;
grab : id option;
capture : capture;
focus : Focus.t;
open_menu : id option;
keys_before : string list;
pressed : string list;
cursor : int;
dy : float;
dragged : float;
moved : float;
painted : Widget.paint list;
overlay : Widget.paint list;
fields : Widget.box list;
}
let empty =
{
input = Widget.no_input;
theme = Theme.default;
was_down = false;
press = false;
was_rdown = false;
rpress = false;
grab = None;
capture = Free;
focus = Focus.none;
open_menu = None;
keys_before = [];
pressed = [];
cursor = 0;
dy = 0.;
dragged = 0.;
moved = 0.;
painted = [];
overlay = [];
fields = [];
}
let at_the_end = max_int
let frame (input : Widget.input) (t : t) =
let press = input.mdown && not t.was_down in
let capture =
if press then Elsewhere
else if input.mdown then t.capture
else if t.was_down then t.capture
else Free
in
let pressed = List.filter (fun k -> not (List.mem k t.keys_before)) input.keys in
let focus = Focus.frame t.focus in
let tab = List.mem "Tab" pressed in
let shift = List.mem "Shift" input.keys in
let focus =
if tab then if shift then Focus.previous focus else Focus.next focus else focus
in
let focus =
match capture with
| Held who when input.mclick ->
if List.exists (fun b -> Widget.id b = who && Widget.contains b input.mx input.my) t.fields then Focus.give who focus
else focus
| _ -> focus
in
let dy = if press then 0. else input.my -. t.input.my in
{
t with
input;
press;
dy;
dragged = (if press then 0. else t.dragged +. dy);
moved = (if press then 0. else t.moved +. Float.abs dy);
was_down = input.mdown;
rpress = input.mrdown && not t.was_rdown;
was_rdown = input.mrdown;
grab = None;
capture;
focus;
keys_before = input.keys;
pressed;
cursor = (if tab then at_the_end else t.cursor);
painted = [];
overlay = [];
fields = [];
}
let paint (t : t) = List.rev t.painted @ List.rev t.overlay
let modal (t : t) = t.open_menu <> None
let theme (t : t) = t.theme
let set_theme theme (t : t) = { t with theme }
let draw (t : t) ps = { t with painted = List.rev_append ps t.painted }
let id = Widget.id
let interact (t : t) (b : Widget.box) =
let i = t.input in
let hot =
Widget.contains b i.mx i.my
&& (match t.open_menu with Some m -> m = Widget.id b | None -> true)
&& (match t.grab with Some m -> m = Widget.id b | None -> true)
in
let capture =
if hot && t.press && t.capture = Elsewhere then Held (id b) else t.capture
in
let mine = capture = Held (id b) in
let held = mine && i.mdown in
let clicked = hot && i.mclick && (mine || capture = Free) in
({ t with capture }, hot, held, clicked)
let label (t : t) (b : Widget.box) s = draw t (Look.label t.theme b s)
let button ?(enabled = true) (t : t) (b : Widget.box) s =
let t, hot, held, clicked = if enabled then interact t b else (t, false, false, false) in
(draw t (Look.button t.theme b s ~hot ~held ~enabled), clicked)
let checkbox (t : t) (b : Widget.box) s checked =
let t, hot, held, clicked = interact t b in
let checked = if clicked then not checked else checked in
(draw t (Look.checkbox t.theme b s ~checked ~hot ~held), checked)
let slider (t : t) (b : Widget.box) ~from ~to_ v =
let t, hot, held, _clicked = interact t b in
let th = t.theme in
let v =
if held then Option.value (Look.slider_value th b ~from ~to_ t.input.mx) ~default:v
else v
in
let fraction = if to_ = from then 0. else max 0. (min 1. ((v -. from) /. (to_ -. from))) in
(draw t (Look.slider th b ~fraction ~hot ~held), v)
let button_size (th : Theme.t) s =
(Widget.text_width ~size:th.text_size s +. (2. *. th.padding), th.row)
let checkbox_size (th : Theme.t) s =
( th.row +. th.padding +. Widget.text_width ~size:th.text_size s +. th.padding,
th.row )
let slider_size (th : Theme.t) = (th.slider_width, th.row)
let knob_travel = 200.
let knob (t : t) (b : Widget.box) ~from ~to_ v =
let t, hot, held, _clicked = interact t b in
let v = if held && not t.press then max (min from to_) (min (max from to_) (v +. (t.dy /. knob_travel *. (to_ -. from)))) else v in
let fraction = if to_ = from then 0. else (v -. from) /. (to_ -. from) in
(draw t (Look.knob t.theme b ~fraction ~hot ~held), v)
let rocker (t : t) (b : Widget.box) on =
let t, hot, held, clicked = interact t b in
let on = if clicked then not on else on in
(draw t (Look.rocker t.theme b ~on ~hot ~held), on)
let selector_step = 24.
let selector (t : t) (b : Widget.box) (labels : string list) (index : int) =
let t, hot, held, clicked = interact t b in
let n = List.length labels in
let t, index =
if held && t.dragged >= selector_step then ({ t with dragged = t.dragged -. selector_step }, min (n - 1) (index + 1))
else if held && t.dragged <= -.selector_step then ({ t with dragged = t.dragged +. selector_step }, max 0 (index - 1))
else if clicked && t.moved < 3. && n > 0 then (t, (index + 1) mod n)
else (t, index)
in
(draw t (Look.selector t.theme b labels ~index ~hot ~held), index)
let knob_size (th : Theme.t) = (th.dial +. 20., th.dial +. 20.)
let rocker_size (th : Theme.t) = (th.row *. 0.6, th.row *. 1.2)
let selector_size (th : Theme.t) labels =
let widest = List.fold_left (fun m s -> max m (Widget.text_width ~size:(th.text_size *. 0.8) s)) 0. labels in
(th.dial +. (2. *. (th.text_size *. 1.3)) +. widest, th.dial +. (2. *. th.text_size *. 1.3) +. th.text_size)
let field ?(enabled = true) (t : t) (b : Widget.box) (text : string) =
let th = t.theme in
let me = id b in
if not enabled then (draw t (Look.field th b text ~caret:None ~enabled:false), text)
else
let t = { t with focus = Focus.saw me t.focus; fields = b :: t.fields } in
let t, _hot, _held, clicked = interact t b in
let cursor = max 0 (min (String.length text) t.cursor) in
let t, cursor =
if clicked then
( { t with focus = Focus.give me t.focus },
Text.byte_of_column text (Look.field_column_at th b text ~caret:cursor t.input.mx) )
else if t.input.mclick && t.capture = Elsewhere then
({ t with focus = Focus.clear t.focus }, cursor)
else (t, cursor)
in
let focused = Focus.has me t.focus in
let pressed k = List.mem k t.pressed in
let text, cursor =
if not focused then (text, cursor)
else Text.edit ~typed:t.input.typed ~pressed text cursor
in
let t = draw t (Look.field th b text ~caret:(if focused then Some cursor else None) ~enabled:true) in
((if focused then { t with cursor } else t), text)
let field_size (th : Theme.t) = (th.field_width, th.row)
let progress (t : t) (b : Widget.box) fraction = draw t (Look.progress t.theme b fraction)
let progress_size (th : Theme.t) = (th.slider_width, th.row *. 0.6)
let (t : t) (b : Widget.box) (items : string list) (chosen : int) =
let th = t.theme in
let me = id b in
let was_open = t.open_menu = Some me in
let t, hot, held, clicked = interact t b in
let n = List.length items in
let under_mouse =
let rec go i =
if i >= n then None
else if Widget.contains (Look.menu_item th b i) t.input.mx t.input.my then Some i
else go (i + 1)
in
if was_open then go 0 else None
in
let chosen = if t.input.mclick then match under_mouse with Some i -> i | None -> chosen else chosen in
let =
if clicked then if was_open then None else Some me
else if was_open && t.input.mclick then None
else t.open_menu
in
let t = { t with open_menu } in
let label = match List.nth_opt items chosen with Some s -> s | None -> "" in
let t = draw t (Look.menu_closed th b label ~hot ~held) in
let t =
if open_menu = Some me then { t with overlay = List.rev_append (Look.menu_items th b items ~under:under_mouse) t.overlay } else t
in
(t, chosen)
let canvas (t : t) (b : Widget.box) =
let t, hot, _held, _clicked = interact t b in
let i = t.input in
let at = (i.mx, i.my) in
let events =
(if hot then [ Widget.Hover at ] else [])
@ (if hot && t.press && t.capture = Held (id b) then [ Widget.Press at ] else [])
@ if hot && t.rpress then [ Widget.Right_press at ] else []
in
(t, events)
let (t : t) at items =
let th = t.theme in
let b = Look.context_box th at items in
let under =
List.find_opt (fun k -> Widget.contains (Look.menu_item th b k) t.input.mx t.input.my) (List.init (List.length items) Fun.id)
in
let answer = if t.input.mclick then match under with Some k -> `Chosen k | None -> `Dismissed else `Open in
let t = { t with grab = Some (id b); capture = (if t.press then Held (id b) else t.capture) } in
({ t with overlay = List.rev_append (Look.menu_items th b items ~under) t.overlay }, answer)
let list (t : t) (b : Widget.box) items selected =
let th = t.theme in
let t, _hot, _held, clicked = interact t b in
let selected =
if clicked then
let i = int_of_float ((Widget.top b -. t.input.my) /. th.row) in
if i >= 0 && i < List.length items then Some i else selected
else selected
in
(draw t (Look.list th b items ~selected), selected)
let list_size (th : Theme.t) = (th.field_width, th.row *. 5.)
let (th : Theme.t) items =
let widest =
List.fold_left (fun acc s -> max acc (Widget.text_width ~size:th.text_size s)) 0. items
in
(widest +. (3. *. th.padding), th.row)
let text_area (t : t) (b : Widget.box) (edit : Text_edit.t) =
let th = t.theme in
let me = id b in
let t = { t with focus = Focus.saw me t.focus } in
let t, _hot, held, clicked = interact t b in
let width = Look.columns th b in
let caret_line, _ = Text_edit.place ~width edit (Text_edit.caret edit) in
let first = max 0 (caret_line - Look.rows th b + 1) in
let offset_at x y =
let line, column = Look.text_area_place th b ~first x y in
Text_edit.offset ~width edit ~line ~column
in
let t, edit =
if clicked then
({ t with focus = Focus.give me t.focus }, Text_edit.at (offset_at t.input.mx t.input.my) edit)
else if held then (t, Text_edit.to_ (offset_at t.input.mx t.input.my) edit)
else if t.input.mclick && t.capture = Elsewhere then ({ t with focus = Focus.clear t.focus }, edit)
else (t, edit)
in
let focused = Focus.has me t.focus in
let pressed k = List.mem k t.pressed in
let shift = List.mem "Shift" t.input.keys in
let control = List.mem "Control" t.input.keys in
let edit =
if not focused then edit
else begin
let move pos e = if shift then Text_edit.to_ pos e else Text_edit.at pos e in
let text = Text_edit.to_string edit in
let caret = Text_edit.caret edit in
let line, column = Text_edit.place ~width edit caret in
let edit =
if control && pressed "z" then if shift then Text_edit.redo edit else Text_edit.undo edit
else if control && pressed "y" then Text_edit.redo edit
else if t.input.typed <> "" && not control then Text_edit.insert t.input.typed edit
else if pressed "Enter" then Text_edit.insert "\n" edit
else if pressed "Backspace" then Text_edit.delete_backward edit
else if pressed "Delete" then Text_edit.delete_forward edit
else if pressed "ArrowLeft" then move (Text.prev_char text caret) edit
else if pressed "ArrowRight" then move (Text.next_char text caret) edit
else if pressed "ArrowUp" then move (Text_edit.offset ~width edit ~line:(line - 1) ~column) edit
else if pressed "ArrowDown" then move (Text_edit.offset ~width edit ~line:(line + 1) ~column) edit
else if pressed "Home" then move (Text_edit.offset ~width edit ~line ~column:0) edit
else if pressed "End" then move (Text_edit.offset ~width edit ~line ~column:width) edit
else edit
in
edit
end
in
let lines = Text_edit.lines ~width edit in
let caret_line, caret_column = Text_edit.place ~width edit (Text_edit.caret edit) in
let first = max 0 (caret_line - Look.rows th b + 1) in
let t =
draw t
(Look.text_area th b lines ~range:(Text_edit.range edit)
~caret:(if focused then Some (caret_line - first, caret_column) else None)
~first)
in
(t, edit)
let text_area_size (th : Theme.t) = (th.field_width *. 1.6, th.row *. 5.)