-
Notifications
You must be signed in to change notification settings - Fork 16
/
b_layout.ml
2928 lines (2619 loc) · 110 KB
/
b_layout.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
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
(* This file is part of BOGUE, by San Vu Ngoc *)
(* Layout is the main object type. *)
(* a layout is a 'box' which can contain 'sub-boxes'. We use the
terminology of houses: a house contains several rooms. Each room
can be viewed as a house which contains other rooms etc. Thus, this
is a simple graph with variable degree. A leaf (a room which does
not contain subrooms) is called a resident; it contains a
Widget. In the whole (connected) tree, the summit is the main
layout: the only one which does not belong to any house; it is
called the top_house, and corresponds to a "physical" SDL window.
The size of the SDL window should always match the size of the
top_house. *)
(* Warning: a widget should *not* appear twice (or more) inside a
Layout. Otherwise, the results are not going to be satisfactory: a
widget is associated to a geometry in a layout. Instead one should
use two differents widgets with a connection between them to
synchronize the data. *)
open Tsdl
open B_utils
module Avar = B_avar
module Box = B_box
module Chain = B_chain
module Draw = B_draw
module Label = B_label
module Mouse = B_mouse
module Selection = B_selection
module Slider = B_slider
module Style = B_style
module Sync = B_sync
module Theme = B_theme
module Time = B_time
module Trigger = B_trigger
module Tvar = B_tvar
module Var = B_var
module Widget = B_widget
type background =
(* TODO instead we should keep track of how the box was
created... in case we want to recreate (eg. use it for another
window...?) *)
| Style of Style.t
| Box of Box.t
let color_bg color =
Style (Style.(of_bg (color_bg color)))
let opaque_bg color = color_bg Draw.(opaque color)
let theme_bg = opaque_bg @@ Draw.find_color Theme.bg_color
let style_bg s =
Style s
let box_bg b =
Box b
type adjust =
| Fit
| Width
| Height
| Nothing
type transform = {
angle : float Avar.t;
center : (Sdl.point option) Avar.t;
flip : Sdl.flip Avar.t;
alpha : float Avar.t
}
type geometry = {
x : int Avar.t;
y : int Avar.t;
(* The (x,y) coords define the position of the layout wrt its
container (the house). Origin is top-left. *)
w : int Avar.t;
h : int Avar.t;
voffset : (int Avar.t) Var.t;
(* The [voffset] is the vertical offset = the y value of where the content of
the layout will be drawn. It is typically used for scrolling. It is similar
to the 'y' variable', except that:
1. the clipping rect (if defined) is *not* translated in case of voffset
2. the background is not translated either *)
transform: transform;
}
type current_geom = {
x : int;
y : int;
w : int;
h : int;
voffset : int
}
(* convert between same type in Draw... *)
let to_draw_geom (g : current_geom) =
{ Draw.x = g.x; Draw.y = g.y; Draw.w = g.w; Draw.h = g.h;
Draw.voffset = g.voffset }
type room_content =
| Rooms of room list
(* In principle, rooms in a house with the same layer should have
non-intersecting geometries, otherwise it is not clear which one gets the
mouse focus (this can be violated, eg. with Layout.superpose). Popups are
drawn on a different layer. *)
| Resident of Widget.t
and room = {
id : int; (* unique identifier. *)
name : string option;
(* If needed for debugging, one can give a name to the room. *)
lock : Mutex.t;
(* Lock for concurrent access by several threads. *)
mutable thread_id : int;
(* Id of the thread holding the lock. TODO use Var.t instead. *)
adjust : adjust;
(* should we adjust the size of this room to fit its content? *)
(* not implemented yet *)
mutable resize : ((int * int) -> unit);
(* The [resize] function is called when the house changed size. (int * int)
is the house size (w,h). *)
mutable show : bool; (* should we show this room? *)
mutable hidden : bool;
(* The [hidden] field is only useful when [t.show = true]. Then [t.hidden =
true] if the layout is currently not displayed onscreen. (Upon creation,
all layouts are hidden.) Only used to correctly detect if animations are
running. This field is only set by the Layout.display function, it should
not be modified by user. Note that t.show has precedence for being
hidden: it [t.show = false], then t is hidden no matter what [t.hidden]
says. *)
mutable geometry : geometry;
(* [geometry] contains the relative geometry of the room wrt the house. All
components are dynamic variables, that need to be recomputed at each
iteration. Note: rooms inside a house must be physically inside the
geometry of the house. If not, they will not be detected by the mouse,
for instance. *)
mutable current_geom : current_geom;
(* [current_geom] is the current *absolute* geometry. Is updated at each
display. But because of clip, the actual rendered size can be smaller
than indicated size. Before the start of the main loop, it is equal to
the initial values of the geometry field. *)
(* A special case of current_geom.(x,y) is to specify window position for
the top layouts. See set_window_pos. *)
mutable clip : bool;
(* If [clip]=true, the room (and its children) will be clipped inside its
geometry. This should be set whenever one want to scroll the content of
the layout inside the layout. This is also used (and set) by hide/show
animations. TODO replace this by a more flexible 'overflow'
specification. *)
mutable background : background option;
mutable shadow : Style.shadow option;
mask : Sdl.surface option;
(* If there is a mask, a position (x,y) will be declared inside the layout
if it corresponds to a mask pixel with alpha value <> 0. A mask will act
as a clip if it is uniformly white, and the shape is given by nonzero
alpha values. (TODO) *)
mutable content : room_content;
mutable layer : Draw.layer;
(* [layer] is the particular layer = chain element of this layout. It should
never be an empty layer (Chain.None), except for the special layout that
contains all windows. If a room contains other Rooms, its layer should be
at least as deep as the layers of the Rooms, otherwise the "background"
might end-up not being at the background... *)
(* In principle a chain of layers is attached to a window. When creating a
new window, one has to select a new layer chain (use_new_layer). *)
mutable canvas : Draw.canvas option;
(* The canvas contains the "hardware" information to render the room *)
(* The canvas is not really an intrinsic property of the layout, it is used
only when rendering is required. It may change "without notice" when a
layout is copied into another window. It is first initialized by
[make_window]. *)
mutable house: room option;
(* [house] = parent: this is the "room" that contains this room in his
"Rooms". This field is mutable because of cyclic definition: one cannot
set the house before defining it... It is our responsibility to make sure
that the house really corresponds to the parent element, in order to
avoid cycles etc. *)
(* cache : Sdlvideo.surface; *) (* ou texture? mettre un cache pour
accélerer l'affichage, plutôt que
d'effacer tout à chaque itération ? *)
mutable mouse_focus : bool; (* set interactively when has mouse focus *)
mutable keyboard_focus : bool option;
(* None = cannot have focus; Some b = has focus or not *)
(* TODO: should we move the keyboard_focus to the Widget? A layout which
contains a Rooms list cannot really have keyboard_focus...and in fact it
will not be detected by 'next_keyboard' *)
(* TODO : mutable draggable : int option; *) (* None = not draggable; Some
delay = drag after delay (in ms) *)
mutable draggable : bool;
(* TODO keep_focus_on_pressed: bool (default = true) CF. menu2. BUT It's not
so easy because many layouts can cover a widget. Ideally, this property
should belong to the widget. *)
mutable removed : bool;
(* [removed] is an experimental field: hint that the layout should not be
used anymore by the board, at least temporarily. Maybe show/hidden could
be used instead. *)
}
type t = room
(* The whole connected component of a layout is a tree, whose vertices (nodes)
are rooms and leaves are widgets (=Resident). The number of branches (=Rooms
list) from a vertex is arbitrary. The house field gives the parent of a
vertex.
There are several interesting ways of going through a tree:
- through every vertex
- only through leaves
- only leaves at a common level (=same generation number)
- nearest neighbour (left, right, up, or down) in the planar embedding
*)
(* We use words "room", "layout", and "house" for the same type of object.
- "layout" will in general refer to the main house, ie containing everything
that is displayed on the window.
- "house" in general refers to a parent of some room, ie an object contaning
sub-rooms.
- "room" is the generic term for sub objects contained in the general layout.
*)
exception Fatal_error of (t * string)
exception Found of t
(* [not_specified] is a special value used to indicate that the window position
should be guessed by the program. *)
let not_specified = Sdl.Window.pos_undefined
let no_clip = ref false
(* The normal behaviour when a non-zero voffset is specified is to clip the
layout to the original rectangle. This permits the show/hide
animation. Setting [no_clip = true] can be a good idea for debugging
graphics. *)
let draw_boxes = Widget.draw_boxes
(* this is only used for debugging. This can slow down rendering quite a bit *)
let equal r1 r2 = r1.id = r2.id
let (==) = equal
let sprint_id r =
Printf.sprintf "#%u%s" r.id (match r.name with
| None -> ""
| Some s -> Printf.sprintf " (%s)" s)
module Hash = struct
type t = room
let equal = equal
let hash room = room.id
end
module WHash = Weak.Make(Hash)
(* [rooms_wtable] this is a weak set of all created rooms, searchable by their
unique id. It is weak in the sense that rooms can be reclaimed by the GC when
not anymore in use, and automatically disappear from the set. *)
let rooms_wtable = WHash.create 50
(* [cemetery] is only for debugging: we insert here the room ids we think are
not used anymore. Then we can check if the GC did remove them from the
[rooms_wtable]. *)
let cemetery = ref []
let send_to_cemetery room =
cemetery := room.id :: !cemetery
let rec remove_wtable room =
if WHash.mem rooms_wtable room
then begin
printd debug_memory "Removing room %s from Wtable" (sprint_id room);
WHash.remove rooms_wtable room;
if WHash.mem rooms_wtable room
then begin
printd debug_error
"Several instances of room %s are registered in the weak hash table."
(sprint_id room);
remove_wtable room;
(* The hash can host several instances of the room. However this signals
a bug somewhere. *)
end;
send_to_cemetery room;
end
let clear_wtable () = WHash.clear rooms_wtable
(* let rooms_table : (int, room) Hashtbl.t = Hashtbl.create 50;;*)
(* this is where we store the reverse lookup: room.id ==> room *)
(* of course the problem is to free this when rooms are not used anymore... to
prevent a memory leak. *)
(* TODO use weak tables (or Ephemerons???) *)
(* https://caml.inria.fr/pub/docs/manual-ocaml/libref/Weak.html *)
(* let of_id id = *)
(* try Hashtbl.find rooms_table id with *)
(* | Not_found -> failwith (Printf.sprintf "Cannot find room with id=%d" id);; *)
(* if !debug is true, we replace the background by solid red *)
let delete_background room =
printd debug_memory "Delete background for room %s" (sprint_id room);
do_option room.background
(fun b ->
let () =
room.background <-
if !debug then Some (opaque_bg Draw.red) else None in
match b with
| Style s -> Style.unload s
| Box b -> Box.unload b)
(* this can be used to force recreating the background, for instance after
changing the size of the room *)
let unload_background room =
do_option room.background (function
| Box b -> Box.unload b
| Style s -> Style.unload s) (* maybe not necessary *)
(* WARNING: in "iter" and in all other search functions below, recall that
itering though a room is tricky because of mutability and threading. The
structure of the tree can be changed by another thread while we iter. Most
dangerous: it can also be changed by the itering itself, hehe. If necessary,
doing "iter lock room" should minimize the risk (but not 100%: the tree can
still be modified while we are locking..) *)
(* iter through all the rooms (layouts & widgets) contained in the [room],
including the initial [room] itself. *)
(* top to bottom *)
let rec iter f room =
f room;
match room.content with
| Resident _ -> ()
| Rooms list -> List.iter (iter f) list
(* iter through widgets *)
let rec iter_widgets f room =
match room.content with
| Resident w -> f w
| Rooms list -> List.iter (iter_widgets f) list
let map_widgets f room =
let list = ref [] in
iter_widgets (fun w -> list := (f w) :: !list) room;
!list
(* iter the direct children *)
let iter_rooms f house =
match house.content with
| Resident _ -> printd (debug_error + debug_board)
"Layout %s has no rooms: cannot iter." (sprint_id house)
| Rooms list -> List.iter f list
(* returns the list of rooms of the layout, or Not_found if there is a
resident *)
let get_rooms layout =
match layout.content with
| Resident _ ->
printd debug_error
"[Layout.get_rooms] This layout %s is a leaf, not a node: it does not \
contain a list of rooms" (sprint_id layout);
raise Not_found
| Rooms list -> list
let siblings room =
match room.house with
| None ->
printd debug_error
"Cannot get siblings of room %s because it does not belong to any \
house." (sprint_id room);
[]
| Some house -> get_rooms house
let rec belongs_to ~parent room =
match room.house with
| None -> false
| Some h -> equal h parent || belongs_to ~parent h
(* return the resident widget, or Not_found *)
let widget layout =
match layout.content with
| Rooms _ ->
printd debug_error
"This room %s is a node, not a leaf: \
it does not contain a resident widget" (sprint_id layout);
raise Not_found
(* or, return the first available widget with next_widget? *)
| Resident w -> w
let get_resident = widget
(* return the first resident widget with show=true inside the layout, or
Not_found *)
let rec first_show_widget layout =
if layout.show
then match layout.content with
| Resident w ->
(printd debug_board "first_show_widget selects %u" (Widget.id w); w)
| Rooms rooms ->
let rec loop = function
| [] -> raise Not_found
| r::rest -> try first_show_widget r with Not_found -> loop rest in
loop rooms
else raise Not_found
(* use this to reset all widget textures (room + all children) for reducing
memory. The layout can still be used without any impact, the textures will be
recreated on the fly. If you want to really remove all created textures, you
have to use delete_backgrounds too; but then the backgrounds will *not* be
recreated. *)
let unload_widget_textures room =
unload_background room;
iter_widgets Widget.unload_texture room
(* same, but for all rooms + widgets *)
let unload_textures room =
let f r =
unload_background r;
match r.content with
| Resident w -> Widget.unload_texture w
| _ -> () in
iter f room
let delete_backgrounds room =
iter delete_background room
let delete_textures room =
unload_textures room;
delete_backgrounds room
let finalize room =
printd debug_memory "Finalize room %s" (sprint_id room);
delete_textures room
(* Return the list of all texts contained in the widgets *)
let get_texts room =
map_widgets Widget.get_text room
|> List.filter (fun s -> s <> "")
let get_text room =
get_resident room
|> Widget.get_text
let set_text room text =
Widget.set_text (get_resident room) text
(* Pressing the TAB key in the main loop will switch the keyboard focus to
another room. Here we save the room that had keyboard focus just before
pressing TAB. This global variable should be thread safe because it is
modified only by the main loop. Another option could be to store the room_id
in an event. (?) *)
let keyboard_focus_before_tab : t option ref = ref None
let fresh_id = fresh_int ()
(** make geometry *)
let geometry ?(x=0) ?(y=0) ?(w=0) ?(h=0) ?(voffset=0) ?transform () : geometry =
{ x = Avar.var x;
y = Avar.var y;
w = Avar.var w;
h = Avar.var h;
voffset = Var.create (Avar.var voffset);
transform = default transform
{ angle = Avar.var 0.;
center = Avar.var None;
flip = Avar.var Sdl.Flip.none;
alpha = Avar.var 1.}
}
(** list of all integer dynamical variables *)
let get_int_avars room =
let g = room.geometry in [g.x; g.y; g.w; g.h; Var.get g.voffset]
let current_geom ?(x=0) ?(y=0) ?(w=0) ?(h=0) ?(voffset=0) () : current_geom =
{ x; y; w; h; voffset}
(* Transform geometry into current_geom *)
let to_current_geom (g : geometry) : current_geom =
{ x = Avar.get g.x;
y = Avar.get g.y;
w = Avar.get g.w;
h = Avar.get g.h;
voffset = Avar.get (Var.get g.voffset) }
(* get current layer of layout *)
let get_layer l =
l.layer
(* [base_layer rooms] returns the deepest layer of the list of rooms, or the
current layer if the list is empty. *)
let base_layer = function
| [] -> Draw.get_current_layer ()
| room::rooms ->
List.fold_left Chain.min (get_layer room) (List.map get_layer rooms)
(* Create a new room. Rather use the [create] function below. *)
let create_unsafe
?name
?(set_house = true) ?(adjust = Fit)
?(resize = fun _ -> ())
?layer
?mask ?background ?shadow ?house ?keyboard_focus ?(mouse_focus=false)
?(show = true) ?(clip = false) ?(draggable = false) ?canvas
geometry content =
let id = fresh_id () in
let layer = match layer with
| Some layer -> layer
| None -> match content with
| Rooms rooms -> base_layer rooms
| Resident _ -> Draw.get_current_layer () in
let room =
{
id;
name;
lock = Mutex.create ();
thread_id = Thread.(id (self ()));
show;
hidden = true;
adjust;
resize;
geometry;
current_geom = to_current_geom geometry;
clip;
mask;
background; (* = (Some (Solid Draw.(opaque blue))); (* DEBUG *) *)
shadow;
content;
layer;
house;
keyboard_focus;
mouse_focus;
canvas;
draggable;
removed = false;
} in
(* we update the lookup table: *)
(* remove is in principle not necessary *)
if !debug
then if WHash.mem rooms_wtable room
then (printd debug_error "A room with same id was already in the table !";
remove_wtable room);
WHash.add rooms_wtable room;
(* we update the resident room_id field *)
(* we update the content's house field *)
let () = match content with
| Resident w -> w.Widget.room_id <- Some id
| Rooms list -> if set_house
then List.iter (fun r -> r.house <- Some room) list in
Gc.finalise finalize room;
(* Should we really do this [finalize]? Because who knows when the Gc will
destroy the background texture.... maybe too late (after renderer was
destroyed, and hence the texture pointer could point to a completely
different texture). In order to prevent this, we call Gc.full_major when
destroying the renderer. *)
printd debug_board "Layout %s created." (sprint_id room);
room
(* The public [create] version. *)
let create = create_unsafe ~set_house:true
(* the dummy room is only used to search the Weak table *)
let dummy_room = create ~name:"dummy" (geometry ()) (Rooms [])
let of_id_unsafe id : room =
try WHash.find rooms_wtable {dummy_room with id} with
| Not_found ->
printd debug_warning "Cannot find room with id=%d" id;
raise Not_found
(* A detached room is a layout that does not belong to the current layout tree,
and is not associated to any SDL window (so no canvas field). *)
let is_detached room =
room.house = None && room.canvas = None
(* Currently [is_removed] is different from [is_detached]. *)
let is_removed room =
room.removed
(* Notify the board that the layout cannot have focus (but it can still belong
to the layout tree). *)
let remove_one room =
printd debug_board "Removing layout %s from focus" (sprint_id room);
room.removed <- true
let remove ?(children = false) room =
if children
then iter remove_one room
else remove_one room;
Trigger.push_remove_layout (room.id)
(* This one is more secure: we check if the layout is not detached. *)
let of_id_opt ?not_found id : room option =
match (WHash.find_opt rooms_wtable {dummy_room with id}) with
| None ->
printd debug_error "Cannot find room with id=%d" id;
do_option not_found run;
None
| Some r as o ->
if is_detached r
then (printd debug_error "Trying to access the detached room #%d" id; None)
else o
(* find the room containing a widget (or None if the widget does not belong to a
room or if the room has disappeared in the air)*)
let containing_widget w =
check_option w.Widget.room_id of_id_opt
let of_wid wid =
let w = Widget.of_id wid in
containing_widget w
(* only for debugging: *)
(* check if rooms sent to cemetery have effectively been removed by GC *)
let check_cemetery () =
let check id = try
let r = of_id_unsafe id in
printd debug_memory
"Dead room %s seems to be living. Beware of zombies." (sprint_id r);
false
with
| Not_found ->
printd debug_memory
"Dead room #%u was correctly burried by the GC. RIP." id;
true
in
let rec loop list newlist empty = (* easier to use a Queue *)
match list with
| [] -> empty, newlist
| id::rest ->
if check id
then loop rest newlist empty
else loop rest (id :: newlist) false in
let empty, newlist = loop !cemetery [] true in
cemetery := newlist;
empty
(* Kind of recursive Mutex. Bad style? TODO remove this necessity...
Here we lock the layout to make it available only by the locking
thread. Hence two consecutive locks by the same thread will not
block. TODO mutualize with Var? Probably better to use protect_fn
anyways. *)
let lock l =
if Mutex.try_lock l.lock
then begin
let id = Thread.(id (self ())) in
printd debug_thread "Locking room %s for thread #%i." (sprint_id l) id;
l.thread_id <- id
end
else (* then it was already locked *)
if Thread.(id (self ())) <> l.thread_id (* not same thread, we must wait *)
then begin
let id = Thread.(id (self ())) in
printd debug_thread "Waiting for thread #%i to remove lock for room %s"
id (sprint_id l);
Mutex.lock l.lock;
l.thread_id <- id
end
else begin
printd (debug_thread + debug_error + debug_user)
"!! Layout %s was locked, but by the same thread: we \
continue, but this should be corrected."
(sprint_id l)
end
let unlock l =
printd debug_thread "Unlocking layout %s" (sprint_id l);
if Mutex.try_lock l.lock
then printd debug_thread " (but layout %s was already unlocked)." (sprint_id l);
Mutex.unlock l.lock
(* get the renderer of the layout *)
let renderer t = match t.canvas with
| Some c -> c.Draw.renderer
| _ -> failwith "Cannot get renderer because no canvas was defined"
(* get the Sdl window of the layout *)
let window_opt t =
map_option t.canvas (fun c -> c.Draw.window)
let window t =
match window_opt t with
| Some w -> w
| _ -> begin
printd debug_error
"Cannot get window for layout %s because no canvas was defined"
(sprint_id t);
raise Not_found
end
(* return the top-level layout *)
(* This is relevent only once the main loop has started. Before this, the
top_house is not even created, so it will not return what you expect. *)
let rec top_house layout =
match layout.house with
| None -> layout
| Some r -> top_house r
(* see [top_house] *)
let guess_top () =
try WHash.iter (fun r ->
if not (is_detached r) then raise (Found r)) rooms_wtable;
None with
| Found r -> Some (top_house r)
let is_top layout =
layout.house = None && layout.canvas <> None
(* Shoud this be a public function? The house it not always the one we
imagine. For instance [make_clip] will change the room. Maybe we should
enforce in [make_clip] and others that the original room should not belong to
a house to start with. *)
let get_house layout =
layout.house
let get_content layout =
layout.content
let get_canvas l =
match l.canvas with
| Some c -> c
| None ->
raise (Fatal_error
(l, Printf.sprintf "The room #%d is not associated with any canvas"
l.id))
(* test if layouts share the same layer (= same depth) *)
let same_layer l1 l2 =
Chain.(get_layer l1 == get_layer l2)
let same_stack l1 l2 =
Chain.(same_stack (get_layer l1) (get_layer l2))
(* get the layout background *)
let get_background l =
l.background
(* force compute background at current size. Canvas must be created *)
let compute_background room =
do_option room.background (
fun bg ->
let g = room.current_geom in
Sdl.log "COMPUTE BG w=%u h=%u" g.w g.h;
let box = match bg with
| Style style ->
let b = Box.(create ~width:g.w ~height:g.h ~style ()) in
room.background <- (Some (Box b));
b
| Box b -> Box.unload b; b in
ignore (Box.display (get_canvas room) (get_layer room) box
(Draw.scale_geom (to_draw_geom g))))
(* Change background. *)
(* can be called by a thread *)
(* Remark: one should not set a "Box" background (for safety, because one cannot
use a background of type Box in case the box already belongs to another
room...) *)
let set_background l b =
unload_background l;
l.background <- b
let set_shadow l s =
l.shadow <- s
(** get size of layout *)
let get_size l =
l.current_geom.w, l.current_geom.h
let get_physical_size l =
get_size l |> Draw.scale_size
(** get width of layout *)
let width l =
l.current_geom.w
(** get height *)
let height l =
l.current_geom.h
let resize room =
do_option (get_house room) (fun house ->
room.resize (get_size house))
let disable_resize room =
room.resize <- (fun _ -> ())
let on_resize room f =
let r = room.resize in
room.resize <- (fun house_size -> r house_size; f ())
let fix_content house =
iter_rooms disable_resize house
let resize_content room =
match room.content with
| Rooms list -> List.iter resize list
| Resident w -> Widget.resize w (get_size room)
(* l must be the top house *)
let adjust_window_size l =
if not (is_top l)
then printd debug_error
"[adjust_window_size] should only be called with a top house, what %s is \
not." (sprint_id l)
else if l.canvas <> None
then let w,h = get_physical_size l in
let win = window l in
if (w,h) <> Draw.get_window_size win
then Draw.set_window_size win ~w ~h
else printd debug_graphics
"Window for layout %s already has the required size."
(sprint_id l)
(* Change the size of the room. By default this will cancel the resize function
of this room. If [set_size] or its derivatives [set_width] and [set_height]
are used as part of a layout resize function of the same room, this default
behaviour should be disabled to prevent the resize function to cancel itself:
use [keep_resize:true]. *)
(* TODO faire un module Resize avec keep_resize=true par défaut. *)
let set_size ?(keep_resize = false) ?(check_window = true)
?(update_bg = false) ?w ?h l =
let () = match w,h with
| Some w, Some h ->
l.current_geom <- { l.current_geom with w; h };
Avar.set l.geometry.h h;
Avar.set l.geometry.w w
| Some w, None ->
l.current_geom <- { l.current_geom with w };
Avar.set l.geometry.w w
| None, Some h ->
l.current_geom <- { l.current_geom with h };
Avar.set l.geometry.h h
| None, None -> () in
if update_bg && l.canvas <> None then compute_background l;
(* = ou plutot unload_background ?? *)
if not keep_resize then disable_resize l;
if check_window && is_top l then adjust_window_size l;
resize_content l
let set_height ?keep_resize ?check_window ?update_bg l h =
set_size ?keep_resize ?check_window ?update_bg ~h l
let set_width ?keep_resize ?check_window ?update_bg l w =
set_size ?keep_resize ?check_window ?update_bg ~w l
(* The public version of [set_size] *)
let set_size ?keep_resize ?check_window ?update_bg l (w,h) =
set_size ?keep_resize ?check_window ?update_bg ~w ~h l
(* get voffset *)
let get_voffset l =
(* l.current_geom.voffset;; *)
Avar.get (Var.get l.geometry.voffset)
(** get current absolute x position (relative to the top-left corner of the
window). Not necessarily up-to-date. *)
let xpos l =
l.current_geom.x
(** get current absolute y position *)
let ypos l =
l.current_geom.y
(* left absolute coordinate of the layout's house *)
let x_origin l = match l.house with
| None -> 0
| Some h -> xpos h
(* top absolute coordinate of the layout's house *)
let y_origin l = match l.house with
| None -> 0
| Some h -> ypos h
(* position of room relative to house *)
let pos_from house room =
xpos room - xpos house, ypos room - ypos house
(** get current x value. *)
(* WARNING don't use this inside an animation for x ! It will loop
forever. Instead use Avar.old l.geometry.x *)
let getx l =
Avar.get l.geometry.x
let get_oldx l =
Avar.old l.geometry.x
(** get current y value *)
let gety l =
Avar.get l.geometry.y
let get_oldy l =
Avar.old l.geometry.y
(* Change x of layout, without adjusting parent house. Warning, by
default this disables the resize function. *)
(* This is the x coordinate wrt the containing house *)
(* This won't work if there is an animation running (see Avar.set) *)
let setx ?(keep_resize = false) l x =
let x0 = getx l in
l.current_geom <- { l.current_geom with x = l.current_geom.x + x - x0 };
Avar.set l.geometry.x x;
if not keep_resize then disable_resize l
(* :TODO à vérifier, cf dans "flat" et "tower" *)
(* Change y of layout, without adjusting parent house. *)
(* see above *)
let sety ?(keep_resize = false) l y =
let y0 = get_oldy l in
l.current_geom <- { l.current_geom with y = l.current_geom.y + y - y0 };
Avar.set l.geometry.y y;
if not keep_resize then disable_resize l
(* see above *)
(* warning, it the animation is not finished, using Avar.set has almost no
effect *)
let set_voffset l vo =
Avar.set (Var.get l.geometry.voffset) vo;
l.current_geom <- { l.current_geom with voffset = vo }
(* use this to shift the voffset by a constant amount without stopping an
animation *)
let shift_voffset l dv =
Var.update l.geometry.voffset (fun av ->
if Avar.finished av
then begin
let vo = Avar.get av + dv in
Avar.set av vo;
l.current_geom <- { l.current_geom with voffset = vo };
av
end
else Avar.apply (fun y -> y + dv) av)
(* not used... *)
let reset_pos l =
let w,h = get_size l in
let g = geometry ~w ~h () in (* or modify l.geometry fields in-place? *)
l.geometry <- g;
l.current_geom <- to_current_geom g
(* [get_window_pos] is meaningful only when the window is created, that is after
calling Bogue.make. The corresponding SDL window is created only after
Bogue.run. In the meantime, a special use of current_geom is to indicate the
desired window position within the desktop at startup. *)
let get_window_pos layout =
let f x = if x = not_specified then None else Some x in
let x,y = match layout.canvas with
| None -> xpos layout, ypos layout
| Some _ -> Draw.get_window_position (window layout) in
f x, f y
(* see [get_window_pos]. It should be set *after* Bogue.make. Otherwise it has
possibly no effect, or perhaps causes some glitches. TODO test this more
thoroughly. *)
let set_window_pos layout (x,y) =
match layout.canvas with
| None -> let g = layout.current_geom in
layout.current_geom <- { g with x; y }
| Some _ -> Draw.set_window_position (window layout) x y
let get_transform l =
let t = l.geometry.transform in
let angle = Avar.get t.angle in
let center = Avar.get t.center in
let flip = Avar.get t.flip in
let alpha = Avar.get t.alpha in
Draw.make_transform ~angle ?center ~flip ~alpha ()
let get_alpha l =
Avar.get l.geometry.transform.alpha
let draggable l =
l.draggable
let set_draggable l =
l.draggable <- true
let set_clip l =
l.clip <- true
let unset_clip l =
l.clip <- false
let set_show l b =
l.show <- b
let rec_set_show b l =
let rec loop b l =
l.show <- b;
match l.content with
| Resident _ -> ()
| Rooms list -> List.iter (loop b) list in
loop b l
let show_window t =
set_show (top_house t) true;
do_option (window_opt t) Sdl.show_window
let hide_window t =
set_show (top_house t) false;