-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathinit.lua
1575 lines (1386 loc) · 50.7 KB
/
init.lua
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
--
-- Luanti formspec layout engine
--
-- Copyright © 2022 by luk3yx
--
-- This program is free software: you can redistribute it and/or modify
-- it under the terms of the GNU Lesser General Public License as published by
-- the Free Software Foundation, either version 2.1 of the License, or
-- (at your option) any later version.
--
-- This program is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-- GNU Lesser General Public License for more details.
--
-- You should have received a copy of the GNU Lesser General Public License
-- along with this program. If not, see <https://www.gnu.org/licenses/>.
--
local DEBUG_MODE = false
flow = {}
local S = core.get_translator("flow")
local modpath = core.get_modpath("flow")
local Form = {}
local ceil, floor, min, max = math.ceil, math.floor, math.min, math.max
-- Estimates the width of a valid UTF-8 string, ignoring any escape sequences.
-- This function hopefully works with most (but not all) scripts, maybe it
-- could still be improved.
local byte, strlen = string.byte, string.len
local LPAREN = byte("(")
local function naive_str_width(str)
local w = 0
local prev_w = 0
local line_count = 1
local i = 1
-- string.len() is used so that numbers are coerced to strings without any
-- extra checking
local str_length = strlen(str)
while i <= str_length do
local char = byte(str, i)
if char == 0x1b then
-- Ignore escape sequences
i = i + 1
if byte(str, i) == LPAREN then
i = str:find(")", i + 1, true) or str_length
end
elseif char == 0xe1 then
if (byte(str, i + 1) or 0) < 0x84 then
-- U+1000 - U+10FF
w = w + 1
else
-- U+1100 - U+2000
w = w + 2
end
i = i + 2
elseif char > 0xe1 and char < 0xf5 then
-- U+2000 - U+10FFFF
w = w + 2
i = i + 2
elseif char == 0x0a then
-- Newlines: Reset the width and increase the line count
prev_w = max(prev_w, w)
w = 0
line_count = line_count + 1
elseif char < 0x80 or char > 0xbf then
-- Everything except UTF-8 continuation sequences
w = w + 1
end
i = i + 1
end
return max(w, prev_w), line_count
end
local LABEL_HEIGHT = 0.4
local LABEL_OFFSET = LABEL_HEIGHT / 2
local CHAR_WIDTH = 0.21
-- The "current_lang" variable isn't ideal but means that the language will be
-- known inside ScrollableVBox etc
local current_lang
-- get_translated_string doesn't exist in MT 5.2.0 and older
local get_translated_string = core.get_translated_string or function(_, s)
return s
end
local function get_lines_size(lines)
local w = 0
for _, line in ipairs(lines) do
-- Translate the string if necessary
if current_lang and current_lang ~= "" and current_lang ~= "en" then
line = get_translated_string(current_lang, line)
end
w = max(w, naive_str_width(line) * CHAR_WIDTH)
end
return w, LABEL_HEIGHT * #lines
end
local function get_label_size(label)
label = label or ""
if current_lang and current_lang ~= "" and current_lang ~= "en" then
label = get_translated_string(current_lang, label)
end
local longest_line_width, line_count = naive_str_width(label)
return longest_line_width * CHAR_WIDTH, line_count * LABEL_HEIGHT
end
local size_getters = {}
local function get_and_fill_in_sizes(node)
if node.type == "list" then
return node.w * 1.25 - 0.25, node.h * 1.25 - 0.25
end
if node.w and node.h then
return node.w, node.h
end
local f = size_getters[node.type]
if not f then return 0, 0 end
local w, h = f(node)
node.w = node.w or max(w, node.min_w or 0)
node.h = node.h or max(h, node.min_h or 0)
return node.w, node.h
end
function size_getters.container(node)
local w, h = 0, 0
for _, n in ipairs(node) do
local w2, h2 = get_and_fill_in_sizes(n)
w = max(w, (n.x or 0) + w2)
h = max(h, (n.y or 0) + h2)
end
return w, h
end
size_getters.scroll_container = size_getters.container
function size_getters.label(node)
local w, h = get_label_size(node.label)
return w, LABEL_HEIGHT + (h - LABEL_HEIGHT) * 1.25
end
local MIN_BUTTON_HEIGHT = 0.8
function size_getters.button(node)
local x, y = get_label_size(node.label)
return max(x, MIN_BUTTON_HEIGHT * 2), max(y, MIN_BUTTON_HEIGHT)
end
size_getters.button_exit = size_getters.button
size_getters.image_button = size_getters.button
size_getters.image_button_exit = size_getters.button
size_getters.item_image_button = size_getters.button
size_getters.button_url = size_getters.button
function size_getters.field(node)
local label_w, label_h = get_label_size(node.label)
-- This is done in apply_padding as well but the label size has already
-- been calculated here
if not node._padding_top and node.label and #node.label > 0 then
node._padding_top = label_h
end
local w, h = get_label_size(node.default)
return max(w, label_w, 3), max(h, MIN_BUTTON_HEIGHT)
end
size_getters.pwdfield = size_getters.field
size_getters.textarea = size_getters.field
function size_getters.vertlabel(node)
return CHAR_WIDTH, #node.label * LABEL_HEIGHT
end
function size_getters.textlist(node)
local w, h = get_lines_size(node.listelems)
return w, h * 1.1
end
function size_getters.dropdown(node)
return max(get_lines_size(node.items) + 0.3, 2), MIN_BUTTON_HEIGHT
end
function size_getters.checkbox(node)
local w, h = get_label_size(node.label)
return w + 0.4, h
end
local field_elems = {field = true, pwdfield = true, textarea = true}
local function apply_padding(node, x, y)
local w, h = get_and_fill_in_sizes(node)
-- Labels are positioned from the centre of the first line and checkboxes
-- are positioned from the centre.
if node.type == "label" then
y = y + LABEL_OFFSET
elseif node.type == "checkbox" then
y = y + h / 2
elseif field_elems[node.type] and not node._padding_top and node.label and
#node.label > 0 then
-- Add _padding_top to fields with labels that have a fixed size set
local _, label_h = get_label_size(node.label)
node._padding_top = label_h
elseif node.type == "tabheader" and w > 0 and h > 0 then
-- Handle tabheader if the width and height are set
-- I'm not sure what to do with tabheaders that don't have a width or
-- height set.
y = y + h
end
if node._padding_top then
y = y + node._padding_top
h = h + node._padding_top
end
local padding = node.padding
if padding then
x = x + padding
y = y + padding
w = w + padding * 2
h = h + padding * 2
end
node.x, node.y = x, y
return w, h
end
local invisible_elems = {
style = true, listring = true, scrollbaroptions = true, tableoptions = true,
tablecolumns = true, tooltip = true, style_type = true, set_focus = true,
listcolors = true
}
local DEFAULT_SPACING = 0.2
function size_getters.vbox(vbox)
local spacing = vbox.spacing or DEFAULT_SPACING
local width = 0
local y = 0
for _, node in ipairs(vbox) do
if not invisible_elems[node.type] then
if y > 0 then
y = y + spacing
end
local w, h = apply_padding(node, 0, y)
width = max(width, w)
y = y + h
end
end
return width, y
end
function size_getters.hbox(hbox)
local spacing = hbox.spacing or DEFAULT_SPACING
local x = 0
local height = 0
for _, node in ipairs(hbox) do
if not invisible_elems[node.type] then
if x > 0 then
x = x + spacing
end
local w, h = apply_padding(node, x, 0)
height = max(height, h)
x = x + w
end
end
return x, height
end
function size_getters.stack(stack)
local width, height = 0, 0
for _, node in ipairs(stack) do
if not invisible_elems[node.type] then
local w, h = apply_padding(node, 0, 0)
width = max(width, w)
height = max(height, h)
end
end
return width, height
end
function size_getters.padding(node)
core.log("warning", "[flow] The gui.Padding element is deprecated")
assert(#node == 1, "Padding can only have one element inside.")
local n = node[1]
local x, y = apply_padding(n, 0, 0)
if node.expand == nil then
node.expand = n.expand
end
return x, y
end
local align_types = {}
function align_types.fill(node, x, w, extra_space)
-- Special cases
if node.type == "list" or node.type == "checkbox" or node._label_hack then
return align_types.centre(node, x, w, extra_space)
elseif node.type == "label" then
if x == "y" then
node.y = node.y + extra_space / 2
return
end
-- Hack
node.type = "container"
-- Reset bgimg, some games apply styling to all image_buttons inside
-- the formspec prepend
node[1] = {
type = "style",
-- MT 5.1.0 only supports one style selector
selectors = {"_#"},
-- bgimg_pressed is included for 5.1.0 support
-- bgimg_hovered is unnecessary as it was added in 5.2.0 (which
-- also adds support for :hovered and :pressed)
props = {bgimg = "", bgimg_pressed = ""},
}
-- Use the newer pressed selector as well in case the deprecated one is
-- removed
node[2] = {
type = "style",
selectors = {"_#:hovered", "_#:pressed"},
props = {bgimg = ""},
}
node[3] = {
type = "image_button",
texture_name = "blank.png",
drawborder = false,
x = 0, y = 0,
w = node.w + extra_space, h = node.h,
name = "_#", label = node.label,
style = node.style,
}
-- Overlay button to prevent clicks from doing anything
node[4] = {
type = "image_button",
texture_name = "blank.png",
drawborder = false,
x = 0, y = 0,
w = node.w + extra_space, h = node.h,
name = "_#", label = "",
}
node.y = node.y - LABEL_OFFSET
node.label = nil
node.style = nil
node._label_hack = true
assert(#node == 4)
end
if node[w] then
node[w] = node[w] + extra_space
else
core.log("warning", "[flow] Unknown element: \"" ..
tostring(node.type) .. "\". Please make sure that flow is " ..
"up-to-date and the element has a size set (if required).")
node[w] = extra_space
end
end
function align_types.start()
-- No alterations required
end
-- "end" is a Lua keyword
align_types["end"] = function(node, x, _, extra_space)
node[x] = node[x] + extra_space
end
-- Aliases for convenience
align_types.top, align_types.bottom = align_types.start, align_types["end"]
align_types.left, align_types.right = align_types.start, align_types["end"]
function align_types.centre(node, x, w, extra_space)
if node.type == "label" then
return align_types.fill(node, x, w, extra_space)
elseif node.type == "checkbox" and x == "y" then
node.y = (node.h + extra_space) / 2
return
end
node[x] = node[x] + extra_space / 2
end
align_types.center = align_types.centre
-- Try to guess at what the best expansion setting is
local auto_align_centre = {
image = true, animated_image = true, model = true, item_image_button = true
}
function align_types.auto(node, x, w, extra_space, cross)
if auto_align_centre[node.type] then
return align_types.centre(node, x, w, extra_space)
end
if x == "y" or (node.type ~= "label" and node.type ~= "checkbox") or
(node.expand and not cross) then
return align_types.fill(node, x, w, extra_space)
end
end
local expand_child_boxes
local function expand(box)
local x, w, align_h, y, h, align_v
local box_type = box.type
if box_type == "hbox" then
x, w, align_h, y, h, align_v = "x", "w", "align_h", "y", "h", "align_v"
elseif box_type == "vbox" then
x, w, align_h, y, h, align_v = "y", "h", "align_v", "x", "w", "align_h"
elseif box_type == "stack" or
(box_type == "padding" and box[1].expand) then
box.type = "container"
box._enable_bgimg_hack = true
for _, node in ipairs(box) do
if not invisible_elems[node.type] then
local width, height = node.w or 0, node.h or 0
if node.type == "list" then
width = width * 1.25 - 0.25
height = height * 1.25 - 0.25
end
local padding_x2 = (node.padding or 0) * 2
align_types[node.align_h or "auto"](node, "x", "w", box.w -
width - padding_x2)
align_types[node.align_v or "auto"](node, "y", "h", box.h -
height - padding_x2 - (node._padding_top or 0))
end
end
return expand_child_boxes(box)
elseif box_type == "container" or box_type == "scroll_container" then
for _, node in ipairs(box) do
if node.x == 0 and node.expand and box.w then
node.w = box.w
end
expand(node)
end
return
elseif box_type == "padding" then
box.type = "container"
return expand_child_boxes(box)
else
return
end
box.type = "container"
-- Calculate the amount of free space and put expand nodes into a table
local box_h = box[h]
local free_space = box[w]
local expandable = {}
local expand_count = 0
local first = true
for i, node in ipairs(box) do
local width, height = node[w] or 0, node[h] or 0
if not invisible_elems[node.type] then
if first then
first = false
else
free_space = free_space - (box.spacing or DEFAULT_SPACING)
end
if node.type == "list" then
width = width * 1.25 - 0.25
height = height * 1.25 - 0.25
end
free_space = free_space - width - (node.padding or 0) * 2 -
(y == "x" and node._padding_top or 0)
if node.expand then
expandable[node] = i
expand_count = expand_count + 1
elseif node.type == "label" and align_h == "align_h" then
-- Use the image_button hack even if the label isn't expanded
align_types[node.align_h or "auto"](node, "x", "w", 0)
end
-- Nodes are expanded in the other direction no matter what their
-- expand setting is
if box_h > height or (node.type == "label" and
align_v == "align_h") then
align_types[node[align_v] or "auto"](node, y, h,
box_h - height - (node.padding or 0) * 2 -
(y == "y" and node._padding_top or 0), true)
end
end
end
-- If there's any free space then expand the nodes to fit
if free_space > 0 then
local extra_space = free_space / expand_count
for node, node_idx in pairs(expandable) do
align_types[node[align_h] or "auto"](node, x, w, extra_space)
-- Shift other elements along
for j = node_idx + 1, #box do
if box[j][x] then
box[j][x] = box[j][x] + extra_space
end
end
end
elseif align_h == "align_h" then
-- Use the image_button hack on labels regardless of the amount of free
-- space if this is in a horizontal box.
for node in pairs(expandable) do
if node.type == "label" then
align_types[node.align_h or "auto"](node, "x", "w", 0)
end
end
end
expand_child_boxes(box)
end
function expand_child_boxes(box)
-- Recursively expand and remove any invisible nodes
for i = #box, 1, -1 do
local node = box[i]
-- node.visible ~= nil and not node.visible
if node.visible == false then
-- There's no need to try and expand anything inside invisible
-- nodes since it won't affect the overall size.
table.remove(box, i)
else
expand(node)
end
end
end
-- Renders the GUI into hopefully valid AST
-- This won't fill in names
local function render_ast(node, embedded)
local t1 = DEBUG_MODE and core.get_us_time()
node.padding = node.padding or 0.3
local w, h = apply_padding(node, 0, 0)
local t2 = DEBUG_MODE and core.get_us_time()
expand(node)
local t3 = DEBUG_MODE and core.get_us_time()
local res = {
formspec_version = 7,
{type = "size", w = w, h = h},
}
-- TODO: Consider a nicer place to put these parameters
if node.no_prepend and not embedded then
res[#res + 1] = {type = "no_prepend"}
end
if node.fbgcolor or node.bgcolor or node.bg_fullscreen ~= nil then
-- Hack to prevent breaking mods that rely on the old (broken)
-- behaviour of fbgcolor
if node.fbgcolor == "#08080880" and node.bgcolor == nil and
node.bg_fullscreen == nil then
node.bg_fullscreen = true
node.fbgcolor = nil
end
res[#res + 1] = {
type = "bgcolor",
bgcolor = node.bgcolor,
fbgcolor = node.fbgcolor,
fullscreen = node.bg_fullscreen
}
node.bgcolor = nil
node.fbgcolor = nil
node.bg_fullscreen = nil
end
-- Add the root element's background image as a fullscreen one
if node.bgimg and not embedded then
res[#res + 1] = {
type = node.bgimg_middle and "background9" or "background",
texture_name = node.bgimg, middle_x = node.bgimg_middle,
x = 0, y = 0, w = 0, h = 0, auto_clip = true,
}
node.bgimg = nil
end
res[#res + 1] = node
if DEBUG_MODE then
local t4 = core.get_us_time()
print('apply_padding', t2 - t1)
print('expand', t3 - t2)
print('field_close_on_enter', t4 - t3)
end
return res
end
local function chain_cb(f1, f2)
return function(...)
f1(...)
f2(...)
end
end
local function range_check_transformer(items_length)
return function(value)
local num = tonumber(value)
if num and num == num then
num = floor(num)
if num >= 1 and num <= items_length then
return num
end
end
end
end
local function simple_transformer(func)
return function() return func end
end
-- Functions that transform field values into the easiest to use type
local C1_CHARS = "\194[\128-\159]"
local field_value_transformers = {
field = simple_transformer(function(value)
-- Remove control characters and newlines
return value:gsub("[%z\1-\8\10-\31\127]", ""):gsub(C1_CHARS, "")
end),
checkbox = simple_transformer(core.is_yes),
-- Scrollbars do have min/max values but scrollbars are only really used by
-- ScrollableVBox which doesn't need the extra checks
scrollbar = simple_transformer(function(value)
return core.explode_scrollbar_event(value).value
end),
}
-- Field value transformers that depend on some property of the element
function field_value_transformers.tabheader(node)
return range_check_transformer(node.captions and #node.captions or 0)
end
function field_value_transformers.dropdown(node, _, formspec_version)
local items = node.items or {}
if node.index_event and not node._index_event_hack then
return range_check_transformer(#items)
end
-- MT will start sanitising formspec fields on its own at some point
-- (https://github.com/minetest/minetest/pull/14878), however it may strip
-- escape sequences from dropdowns as well. Since we know what the actual
-- value of the dropdown is anyway, we can just enable index_event for new
-- clients and keep the same behaviour
if (formspec_version and formspec_version >= 4) or
(core.global_exists("fs51") and
fs51.monkey_patching_enabled) then
node.index_event = true
-- Detect reuse of the same Dropdown element (this is unsupported and
-- will break in other ways)
node._index_event_hack = true
return function(value)
return items[tonumber(value)]
end
elseif node._index_event_hack then
node.index_event = nil
end
-- Make sure that the value sent by the client is in the list of items
return function(value)
if table.indexof(items, value) > 0 then
return value
end
end
end
function field_value_transformers.table(node, tablecolumn_count)
-- Figure out how many rows the table has
local cells = node.cells and #node.cells or 0
local rows = ceil(cells / tablecolumn_count)
return function(value)
local row = floor(core.explode_table_event(value).row)
-- Tables and textlists can have values of 0 (nothing selected) but I
-- don't think the client can un-select a row so it should be safe to
-- ignore any 0 sent by the client to guarantee that the row will be
-- valid if the default value is valid
if row >= 1 and row <= rows then
return row
end
end
end
function field_value_transformers.textlist(node)
local rows = node.listelems and #node.listelems or 0
return function(value)
local index = floor(core.explode_textlist_event(value).index)
if index >= 1 and index <= rows then
return index
end
end
end
local function default_field_value_transformer(value)
-- Remove control characters (but preserve newlines)
-- Pattern by https://github.com/appgurueu
return value:gsub("[%z\1-\8\11-\31\127]", ""):gsub(C1_CHARS, "")
end
local default_value_fields = {
field = "default",
pwdfield = "default",
textarea = "default",
checkbox = "selected",
dropdown = "selected_idx",
table = "selected_idx",
textlist = "selected_idx",
scrollbar = "value",
tabheader = "current_tab",
}
local sensible_defaults = {
default = "", selected = false, selected_idx = 1, value = 0,
}
local button_types = {
button = true, image_button = true, item_image_button = true,
button_exit = true, image_button_exit = true
}
-- Removes on_event from a formspec_ast tree and returns a callbacks table
local function parse_callbacks(tree, ctx_form, auto_name_id,
replace_backgrounds, formspec_version)
local callbacks
local btn_callbacks = {}
local saved_fields = {}
local tablecolumn_count = 1
for node in formspec_ast.walk(tree) do
if node.type == "container" then
if node.bgcolor then
local padding = node.padding or 0
table.insert(node, 1, {
type = "box", color = node.bgcolor,
x = -padding, y = -padding,
w = node.w + padding * 2, h = node.h + padding * 2,
})
end
if node.bgimg then
local padding = node.padding or 0
table.insert(node, 1, {
type = node.bgimg_middle and "background9" or "background",
texture_name = node.bgimg, middle_x = node.bgimg_middle,
x = -padding, y = -padding,
w = node.w + padding * 2, h = node.h + padding * 2,
})
end
-- The on_quit callback is undocumented and not recommended, it
-- only gets called when the client tells the server that it's
-- closing the form and not when another form is shown.
if node.on_quit then
callbacks = callbacks or {}
if callbacks.quit then
-- HACK
callbacks.quit = chain_cb(callbacks.quit, node.on_quit)
else
callbacks.quit = node.on_quit
end
end
replace_backgrounds = replace_backgrounds or node._enable_bgimg_hack
elseif node.type == "tablecolumns" and node.tablecolumns then
-- Store the amount of columns for input validation
tablecolumn_count = max(#node.tablecolumns, 1)
elseif replace_backgrounds then
if (node.type == "background" or node.type == "background9") and
not node.auto_clip then
node.type = "image"
end
elseif node.type == "scroll_container" then
-- Work around a Minetest bug with scroll containers not scrolling
-- backgrounds.
replace_backgrounds = true
end
local node_name = node.name
if node_name and node_name ~= "" then
local value_field = default_value_fields[node.type]
if value_field then
-- Update ctx.form if there is no current value, otherwise
-- change the node's value to the saved one.
local value = ctx_form[node_name]
if node.type == "dropdown" and (not node.index_event or
node._index_event_hack) then
-- Special case for dropdowns without index_event
local items = node.items or {}
if value == nil then
ctx_form[node_name] = items[node.selected_idx or 1]
else
local idx = table.indexof(items, value)
if idx > 0 then
node.selected_idx = idx
end
end
node.selected_idx = node.selected_idx or 1
elseif value == nil then
-- If ctx.form[node_name] doesn't exist, then check whether
-- a default value is specified.
local default_value = node[value_field]
local sensible_default = sensible_defaults[value_field]
if default_value == nil then
-- If the element doesn't have a default set, set it to
-- the sensible default value and update ctx.form in
-- case the client doesn't send the field value back.
node[value_field] = sensible_default
ctx_form[node_name] = sensible_default
else
-- Update ctx.form to the default value
ctx_form[node_name] = default_value
end
else
-- Set the node's value to the one saved in ctx.form
node[value_field] = value
end
-- Add the corresponding value transformer transformer to
-- saved_fields
local get_transformer = field_value_transformers[node.type]
saved_fields[node_name] = get_transformer and
get_transformer(node, tablecolumn_count,
formspec_version) or
default_field_value_transformer
elseif node.type == "hypertext" then
-- Experimental (may be broken in the future): Allow accessing
-- hypertext fields with "ctx.form.hypertext_name" as this is
-- the most straightforward way of doing it.
saved_fields[node_name] = default_field_value_transformer
end
end
-- Add the on_event callback (if any) to the callbacks table
if node.on_event then
local is_btn = button_types[node.type]
if not node_name then
-- Flow internal field names start with "_#" to avoid
-- conflicts with user-provided fields.
node_name = ("_#%x"):format(auto_name_id)
node.name = node_name
auto_name_id = auto_name_id + 1
elseif btn_callbacks[node_name] or
(is_btn and saved_fields[node_name]) or
(callbacks and callbacks[node_name]) then
core.log("warning", ("[flow] Multiple callbacks have " ..
"been registered for elements with the same name (%q), " ..
"this will not work properly."):format(node_name))
-- Preserve previous behaviour
btn_callbacks[node_name] = nil
if callbacks then
callbacks[node_name] = nil
end
is_btn = is_btn and not saved_fields[node_name]
end
-- Put buttons into a separate callback table so that malicious
-- clients can't send multiple button presses in one submission
if is_btn then
btn_callbacks[node_name] = node.on_event
else
callbacks = callbacks or {}
callbacks[node_name] = node.on_event
end
node.on_event = nil
end
-- Call _after_positioned (used internally for ScrollableVBox)
if node._after_positioned then
node:_after_positioned()
node._after_positioned = nil
end
end
return callbacks, btn_callbacks, saved_fields, auto_name_id
end
local gui_mt = {
__index = function(gui, k)
local elem_type = k
if elem_type ~= "ScrollbarOptions" and elem_type ~= "TableOptions" and
elem_type ~= "TableColumns" then
elem_type = elem_type:gsub("([a-z])([A-Z])", function(a, b)
return a .. "_" .. b
end)
end
elem_type = elem_type:lower()
local function f(t)
t.type = elem_type
return t
end
rawset(gui, k, f)
return f
end,
}
local gui = setmetatable({
embed = function(fs, w, h)
core.log("warning", "[flow] gui.embed() is deprecated")
if type(fs) ~= "table" then
fs = formspec_ast.parse(fs)
end
fs.type = "container"
fs.w = w
fs.h = h
return fs
end,
formspec_version = 0,
}, gui_mt)
flow.widgets = gui
local current_ctx
function flow.get_context()
if not current_ctx then
error("get_context() was called outside of a GUI function!", 2)
end
return current_ctx
end
-- Returns the new index of the affected element
local function insert_style_elem(tree, idx, node, props, sels)
if not next(props) then
-- No properties, don't try and add an empty style element
return idx
end
local base_selector = node.name or node.type
local selectors = {}
if sels then
for i, sel in ipairs(sels) do
local suffix = sel:match("^%s*$(.-)%s*$")
if suffix then
selectors[i] = base_selector .. ":" .. suffix
else
core.log("warning", "[flow] Invalid style selector: " ..
tostring(sel))
end
end
else
selectors[1] = base_selector
end
table.insert(tree, idx, {
type = node.name and "style" or "style_type",
selectors = selectors,
props = props,
})
if not node.name then
-- Undo style_type modifications
local reset_props = {}
for k in pairs(props) do
-- The style table might have substyles which haven't been removed
-- yet
reset_props[k] = ""
end
table.insert(tree, idx + 2, {
type = "style_type",
selectors = selectors,
props = reset_props,
})
end
return idx + 1
end
local function extract_props(t)
local res = {}
for k, v in pairs(t) do
if k ~= "sel" and type(k) == "string" then
res[k] = v
end
end
return res
end
-- I don't like the idea of making yet another pass over the element tree but I
-- can't think of a clean way of integrating shorthand elements into one of the
-- other loops.
local function insert_shorthand_elements(tree)
for i = #tree, 1, -1 do
local node = tree[i]
-- Insert styles
if node.style then
local props = node.style
if #node.style > 0 then
-- Make a copy of node.style without the numeric keys. This
-- avoids modifying node.style in case it's used for multiple