Source file b_text_input.ml
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
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
(** One-line text-editor widget *)
open Tsdl
open B_utils
module Utf8 = B_utf8
module Theme = B_theme
module Var = B_var
module Trigger = B_trigger
module Draw = B_draw
module Label = B_label
module Mouse = B_mouse
type selection =
| Empty
| Start of int
| Active of (int * int);;
type filter = string -> bool
type t =
{ keys : (string list) Var.t;
cursor : (Draw.texture option) Var.t;
cursor_font : (Label.font) Var.t;
cursor_pos : int Var.t;
cursor_char : string;
render : (Draw.texture option) Var.t;
offset : int Var.t;
font : (Label.font) Var.t;
size : int;
active : bool Var.t;
room_x : int Var.t;
selection : selection Var.t;
max_size : int;
prompt : string;
filter : filter;
}
let triggers = Sdl.Event.[text_editing; text_input; key_down; key_up]
let no_filter _ = true
let uint_filter s = List.mem s ["0"; "1"; "2"; "3"; "4"; "5"; "6"; "7"; "8"; "9"]
let and_filter f1 f2 = function
s -> (f1 s) && (f2 s)
let default_font = Label.File !Theme.text_font
let create ?(max_size = 2048) ?(prompt = "Enter text")
?(size = Theme.text_font_size)
?(filter = no_filter) ?(font = default_font) text =
Draw.ttf_init ();
let keys = Utf8.split text in
{ keys = Var.create keys;
cursor = Var.create None;
cursor_font = Var.create (Label.File Theme.fa_font);
cursor_pos = Var.create 0;
cursor_char = Theme.fa_symbol "tint";
render = Var.create None;
offset = Var.create 0;
font = Var.create font;
size;
active = Var.create false;
room_x = Var.create 0;
selection = Var.create Empty;
max_size;
prompt;
filter;
}
let unload ti =
begin
match Var.get ti.render with
| None -> ()
| Some tex -> begin
Var.set ti.render None;
Draw.forget_texture tex
end
end;
match Var.get ti.cursor with
| None -> ()
| Some tex -> begin
Var.set ti.cursor None;
Draw.forget_texture tex
end
let free = unload
let resize _size ti =
unload ti
let text ti = String.concat "" (Var.get ti.keys)
let is_active ti = Var.get ti.active
let clear ti =
let texo = Var.get ti.render in
Var.set ti.render None;
do_option texo Draw.forget_texture
let stop ti =
printd debug_event "Stopping text input";
if Sdl.is_text_input_active () then Sdl.stop_text_input ();
clear ti;
Var.set ti.active false
let unselect ti =
printd debug_board "Removing selection";
Var.set ti.selection Empty
let set ti keys =
if keys <> Var.get ti.keys
then begin
if Var.get ti.selection <> Empty then unselect ti;
let keys =
if List.length keys > ti.max_size
then (printd debug_memory
"Warning: text_input was truncated because it should not exceed \
%u symbols" ti.max_size;
stop ti;
let head, _ = split_list keys ti.max_size in head)
else keys in
Var.set ti.keys keys;
Var.set ti.cursor_pos (min (Var.get ti.cursor_pos) (List.length keys));
clear ti
end
let kill_selection ti =
match Var.get ti.selection with
| Active (n1,n2) -> let head1, tail1 = split_list (Var.get ti.keys) n1 in
let _, tail2 = split_list tail1 (n2-n1) in
Var.set ti.cursor_pos n1;
Var.set ti.selection Empty;
set ti (List.flatten [head1; tail2]);
| _ -> ()
let select_all ti =
printd debug_board "Select all text";
let l = List.length (Var.get ti.keys) in
Var.set ti.selection (Active (0,l));
clear ti
let insert_list ti list =
kill_selection ti;
let x = Var.get ti.cursor_pos in
let head, tail = split_list (Var.get ti.keys) x in
Var.set ti.cursor_pos (x + (List.length list));
set ti (List.flatten [head; list; tail])
let insert ti s =
insert_list ti [s]
let insert_text ti text =
let list = Utf8.split text in
insert_list ti list
let seps = [" "; ";"; "."; ","; "/"; ":"; "\\n"; "\\t"; "\\j"; "?"; "!"]
let find_word ti =
let n = Var.get ti.cursor_pos in
let daeh, tail = split_list_rev (Var.get ti.keys) n in
let rec find_sep ~complement list pos =
match list with
| [] -> pos
| key::rest -> if (not complement && List.mem key seps) || (complement && not (List.mem key seps))
then pos
else find_sep ~complement rest (pos + 1) in
if tail = [] then (printd debug_board "No word found: we are at the end";
Empty)
else let cursor_key = List.hd tail in
let complement = List.mem cursor_key seps in
let left = find_sep ~complement daeh 0 in
let right = find_sep ~complement tail 0 in
printd debug_board "Word found (%d,%d)" left right;
Active (n-left, n+right)
let select_word ti =
let sel = find_word ti
in Var.set ti.selection sel;
clear ti
let ctrl_pressed () =
let m = Sdl.get_mod_state () in
m = Sdl.Kmod.ctrl
|| m = Sdl.Kmod.lctrl
|| m = Sdl.Kmod.rctrl
let start_selection ti =
let n = Var.get ti.cursor_pos in
printd debug_board "Starting text selection at %d" n;
Var.set ti.selection (Start n)
let shift_check_sel ti =
if Trigger.shift_pressed () then
(if Var.get ti.selection = Empty then start_selection ti)
else unselect ti
let backspace ti =
if Var.get ti.selection <> Empty then kill_selection ti
else let x = Var.get ti.cursor_pos in
if x > 0 then
let head, tail = split_list (Var.get ti.keys) (x-1) in
let tail' = match tail with
| [] -> printd debug_error "This should not happen in backspace"; []
| _::rest -> rest in
Var.set ti.cursor_pos (x-1);
set ti (List.flatten [head; tail'])
let left ti =
shift_check_sel ti;
let x = Var.get ti.cursor_pos in
clear ti;
Var.set ti.cursor_pos (max 0 (x-1))
let right ti =
shift_check_sel ti;
let x = Var.get ti.cursor_pos in
clear ti;
Var.set ti.cursor_pos (min (List.length (Var.get ti.keys)) (x+1))
let home ti =
shift_check_sel ti;
clear ti;
Var.set ti.cursor_pos 0
let last ti =
shift_check_sel ti;
clear ti;
Var.set ti.cursor_pos (List.length (Var.get ti.keys))
let activate ti =
printd debug_event "Activating text_input";
if Sdl.is_text_input_active ()
then begin
printd (debug_error + debug_board + debug_event + debug_user)
"You cannot have several Text_input active at the same time."
end;
Sdl.start_text_input ();
Var.set ti.active true;
clear ti
let make_selection ti =
match Var.get ti.selection with
| Empty -> ()
| Start n0 ->
let n = Var.get ti.cursor_pos in
if n <> n0 then (printd debug_board "Make selection [%d,%d]" n0 n;
Var.set ti.selection (Active (min n0 n, max n0 n)))
else (Var.set ti.selection Empty)
| Active _ -> Var.set ti.selection Empty
let selection_text ti =
match Var.get ti.selection with
| Active (n1,n2) -> let _, tail = split_list (Var.get ti.keys) n1 in
let head, _ = split_list tail (n2-n1) in
String.concat "" head
| _ -> ""
let copy ti =
let text = selection_text ti in
if text <> "" then begin
printd debug_memory "Copy to clipboard: [%s]" text;
go (Sdl.set_clipboard_text text)
end
let kill ti =
copy ti;
kill_selection ti
let paste ti =
if Sdl.has_clipboard_text () then
let text = go (Sdl.get_clipboard_text ()) in
insert_text ti text
let receive_key ti ev =
if is_active ti then let
open Sdl.Event in
match Trigger.event_kind ev with
| `Text_input ->
let s = get ev text_input_text in
if ti.filter s then insert ti s
| `Text_editing -> printd debug_event "Text composing mode"
| `Key_down -> (match get ev keyboard_keycode with
| c when c = Sdl.K.backspace -> backspace ti
| c when c = Sdl.K.left -> left ti
| c when c = Sdl.K.right -> right ti
| c when c = Sdl.K.up -> home ti
| c when c = Sdl.K.home -> home ti
| c when c = Sdl.K.down -> last ti
| c when c = Sdl.K.kend -> last ti
| c when c = Sdl.K.return -> stop ti
| c when c = Sdl.K.a && ctrl_pressed () -> select_all ti
| c when c = Sdl.K.c && ctrl_pressed () -> copy ti
| c when c = Sdl.K.x && ctrl_pressed () -> kill ti
| c when c = Sdl.K.v && ctrl_pressed () -> paste ti
| c -> (printd debug_event "==> Key down event discarded.";
printd debug_event "Key=[%s], mod=%u, Keycode:%u"
(Sdl.get_key_name c) (Sdl.get_mod_state ()) c))
| `Key_up -> (match get ev keyboard_keycode with
| c when c = Sdl.K.lshift -> make_selection ti
| c when c = Sdl.K.rshift -> make_selection ti
| c -> (printd debug_event "==> Key up event discarded.";
printd debug_event "Key=[%s], mod=%u, Keycode:%u"
(Sdl.get_key_name c) (Sdl.get_mod_state ()) c))
| _ -> printd debug_warning "Warning: Event should not happen here"
let memo = ref true
let default_size = (128,32)
let left_margin = 2
let bottom_margin = 5
let font ti = Label.get_font_var ti.font (Theme.scale_int ti.size)
let render_key font key color =
let color = Draw.create_color color in
let surf = Draw.ttf_render font key color in
incr Draw.ttf_surfaces_in_memory;
go (Sdl.set_surface_blend_mode surf Sdl.Blend.mode_none);
go (Sdl.set_surface_rle surf true);
surf
let render_key, render_key_cleanup =
if !memo then let f,table = memo3 render_key in
let cleanup () =
printd debug_graphics "Cleaning up %u SDL_TTF surfaces..."
(Hashtbl.length table);
Hashtbl.iter (fun _ surf ->
Draw.free_surface surf;
decr Draw.ttf_surfaces_in_memory) table;
Hashtbl.clear table in
f, cleanup
else render_key, fun () -> ()
let () = Draw.at_cleanup render_key_cleanup
(** Return size of rendered text. It seems that Sdl.TTF.size_utf8 does not
always give the exact same result as size of blended-rendered
surface. Warning: thus, should use this only on single letters ! *)
let text_dims font text =
if text = ""
then (printd debug_warning "[text_dims] called on empty string"; 0,0)
else let w,h =
Label.physical_size_text font text
in
printd debug_graphics "Size of '%s' = (%d,%d)." text w h;
w,h
let text_dims = memo2 text_dims
let bmax (x,y) (xx,yy) = imax x xx, imax y yy
let size ti =
let w,h =
match Var.get ti.render with
| Some tex -> Draw.tex_size tex
| None -> bmax (text_dims (font ti) (ti.prompt))
(text_dims (font ti) (String.concat "" (Var.get ti.keys))) in
let w,h = Draw.unscale_size (w,h) in
(w + 2*left_margin ,
h + 2*bottom_margin)
let text_width font s =
let w,_ = text_dims font s in w
let cursor_xpos ?n ti =
let n = match n with
| None -> Var.get ti.cursor_pos
| Some n -> n in
let head, _ = split_list (Var.get ti.keys) n in
List.fold_left (fun s key -> s + text_width (font ti) key) 0 head
let x_to_cursor_old ti x0 =
let room_x = Var.get ti.room_x in
let x0 = x0 - room_x - (Theme.scale_int (left_margin - 2)) + (Var.get ti.offset) in
let l = List.length (Var.get ti.keys) in
let rec loop n =
if n >= l then l
else let cx = cursor_xpos ~n ti in
if cx >= x0 then n else loop (n+1)
in loop 0
let x_to_cursor ti x0 =
let room_x = Var.get ti.room_x in
let char_offset = ti.size/3 in
let x0 = x0 - room_x - (Theme.scale_int (left_margin + char_offset))
+ (Var.get ti.offset) in
let rec loop list cx n =
match list with
| [] -> n
| key::rest ->
if cx >= x0 then n else
let advance, _ = text_dims (font ti) key in
loop rest (cx + advance) (n+1) in
loop (Var.get ti.keys) 0 0
let click_cursor ti ev =
printd debug_event "Click cursor";
let x0u, _ = Mouse.pointer_pos ev in
let x0 = Theme.scale_int x0u in
Var.set ti.cursor_pos (x_to_cursor ti x0);
clear ti
let button_down ti ev =
if is_active ti then
(click_cursor ti ev;
start_selection ti)
let click ti ev =
if is_active ti then begin
if Trigger.was_double_click () then select_word ti
else begin
if Trigger.has_full_click ev then click_cursor ti ev;
make_selection ti
end
end
else if Trigger.has_full_click ev then activate ti
let tab ti ev =
if Sdl.Event.(get ev keyboard_keycode) = Sdl.K.tab then begin
if not (is_active ti) then activate ti;
select_all ti
end
let mouse_select ti ev=
printd debug_event "Mouse selection";
click_cursor ti ev;
clear ti
let draw_keys ?fg font keys =
let color = if keys = [] then Draw.(transp faint_color)
else default fg (10,11,12,255) in
printd debug_graphics "Renders keys";
let rec loop keys surfs w h =
match keys with
| [] -> surfs, w, h
| key::rest -> let surf = render_key font key color in
let dw,h = Sdl.get_surface_size surf in
loop rest ((surf, w) :: surfs) (w+dw) h in
let keys = if keys = [] then [" "] else keys in
let surfs, tw, h = loop keys [] 0 0 in
let surf, _ = List.hd surfs in
printd debug_graphics "Create total surface";
let total_surf = Draw.create_surface ~like:surf tw h in
printd debug_graphics "Blit the letters on the surface";
let rec draw_loop = function
| [] -> ()
| (surf, w) :: rest ->
let dst_rect = Sdl.Rect.create ~x:w ~y:0 ~w:0 ~h:0 in
go (Sdl.blit_surface ~src:surf None ~dst:total_surf (Some dst_rect));
if not !memo then Draw.free_surface surf;
draw_loop rest in
draw_loop surfs;
total_surf
let display canvas layer ti g =
let cursor = match Var.get ti.cursor with
| Some s -> s
| None ->
let csize = imax 3 (2*(Theme.scale_int ti.size)/3) in
let cfont = Label.get_font_var ti.cursor_font csize in
let s = draw_keys cfont [ti.cursor_char] ~fg:Draw.(opaque cursor_color) in
let tex = Draw.create_texture_from_surface canvas.Draw.renderer s in
Var.set ti.cursor (Some tex);
Draw.free_surface s;
tex
in
let cw, _ = Draw.tex_size cursor in
let tex = match Var.get ti.render with
| Some t -> t
| None ->
let start_time = if !debug then Unix.gettimeofday () else 0. in
let keys = Var.get ti.keys in
let fg = if keys <> [] then Draw.(opaque !text_color) else
Draw.(opaque faint_color) in
let keys = if keys = [] && not (is_active ti) then [ti.prompt] else keys in
let surf = draw_keys (font ti) keys ~fg in
let tw,th = Sdl.get_surface_size surf in
let box = Draw.create_surface ~like:surf
(tw + cw + cw/2) (th + Theme.scale_int bottom_margin) in
go (Sdl.set_surface_blend_mode box Sdl.Blend.mode_none);
let rect = Draw.rect_translate (Sdl.get_clip_rect surf) (cw/2, 0) in
go (Sdl.set_surface_blend_mode surf Sdl.Blend.mode_none);
go (Sdl.blit_surface ~src:surf None ~dst:box (Some rect));
(match Var.get ti.selection with
| Active (n1,n2) ->
let x1 = cursor_xpos ~n:n1 ti in
let x2 = cursor_xpos ~n:n2 ti in
let sel_rect = Sdl.Rect.create ~x:x1 ~y:0 ~w:(x2-x1) ~h:th in
let sel_rect_cw = Draw.rect_translate sel_rect (cw/2, 0) in
Draw.fill_rect box (Some sel_rect_cw) Draw.(opaque sel_bg_color);
let sel = draw_keys (font ti) keys ~fg:Draw.(opaque sel_fg_color) in
go (Sdl.set_surface_blend_mode sel Sdl.Blend.mode_blend);
go (Sdl.blit_surface ~src:sel (Some sel_rect) ~dst:box (Some sel_rect_cw))
| Start n1 ->
let x1 = cursor_xpos ~n:n1 ti in
let n2 = Var.get ti.cursor_pos in
let x2 = cursor_xpos ~n:n2 ti in
let sel_rect = Sdl.Rect.create ~x:(min x1 x2) ~y:0
~w:(abs (x2-x1)) ~h:th in
let sel_rect_cw = Draw.rect_translate sel_rect (cw/2, 0) in
Draw.fill_rect box (Some sel_rect_cw) Draw.(opaque grey);
go (Sdl.set_surface_blend_mode surf Sdl.Blend.mode_blend);
go (Sdl.blit_surface ~src:surf (Some sel_rect) ~dst:box (Some sel_rect_cw))
| _ -> ());
Draw.free_surface surf;
if Var.get ti.active then begin
let thick = Theme.scale_int 1 in
let hline = Sdl.Rect.create ~x:(cw/2) ~y:(th )
~w:tw ~h:thick in
Draw.fill_rect box (Some hline) Draw.(transp grey);
let cx = cursor_xpos ti in
let offset = Var.get ti.offset in
let offset = if cx <= offset+cw then max 0 (cx-cw)
else if cx - offset >= g.Draw.w - cw - cw/2
then min tw (cx - g.Draw.w + cw + cw/2)
else offset in
Var.set ti.offset offset
end;
let bw, bh = Sdl.get_surface_size box in
let offset = Var.get ti.offset in
let rect_b = Sdl.Rect.create ~x:offset ~y:0 ~w:(min g.Draw.w (bw - offset)) ~h:bh in
let visible = Draw.create_surface ~like:box ~color:Draw.none
(Sdl.Rect.w rect_b) bh in
go (Sdl.blit_surface ~src:box (Some rect_b) ~dst:visible None);
let tex = Draw.create_texture_from_surface canvas.Draw.renderer visible in
Draw.free_surface box;
Draw.free_surface visible;
Var.set ti.render (Some tex);
printd debug_graphics "Time for creating texture = %f s"
(Unix.gettimeofday () -. start_time);
tex
in
let open Draw in
let area = geom_to_rect g in
Sdl.set_text_input_rect (Some area);
Var.set ti.room_x g.x;
let text_blit = copy_tex_to_layer ~overlay:(Draw.Xoffset 0) ~voffset:g.voffset
canvas layer tex area (g.x + (Theme.scale_int left_margin))
(g.y + (Theme.scale_int bottom_margin)) in
if is_active ti
then
let _,bh = tex_size tex in
let voff = Theme.scale_int 4 in
let cursor_g = { g with x = g.x + Theme.scale_int left_margin +
cursor_xpos ti - Var.get ti.offset;
y = g.y + bh - voff } in
[text_blit; tex_to_layer canvas layer cursor cursor_g]
else [text_blit]