forked from your-land-mirror/minetest-flow
Initial commit
This commit is contained in:
commit
4c7996c915
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
test*.lua
|
||||||
|
*.old
|
18
.luacheckrc
Normal file
18
.luacheckrc
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
max_line_length = 80
|
||||||
|
|
||||||
|
globals = {
|
||||||
|
'formspec_ast',
|
||||||
|
'minetest',
|
||||||
|
'hud_fs',
|
||||||
|
'flow',
|
||||||
|
'dump',
|
||||||
|
}
|
||||||
|
|
||||||
|
read_globals = {
|
||||||
|
string = {fields = {'split', 'trim'}},
|
||||||
|
table = {fields = {'copy', 'indexof'}}
|
||||||
|
}
|
||||||
|
|
||||||
|
-- This error is thrown for methods that don't use the implicit "self"
|
||||||
|
-- parameter.
|
||||||
|
ignore = {"212/self", "432/player", "43/ctx", "212/player", "212/ctx", "212/value"}
|
157
LICENSE.md
Normal file
157
LICENSE.md
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
### GNU LESSER GENERAL PUBLIC LICENSE
|
||||||
|
|
||||||
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
|
Copyright (C) 2007 Free Software Foundation, Inc.
|
||||||
|
<https://fsf.org/>
|
||||||
|
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies of this
|
||||||
|
license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
This version of the GNU Lesser General Public License incorporates the
|
||||||
|
terms and conditions of version 3 of the GNU General Public License,
|
||||||
|
supplemented by the additional permissions listed below.
|
||||||
|
|
||||||
|
#### 0. Additional Definitions.
|
||||||
|
|
||||||
|
As used herein, "this License" refers to version 3 of the GNU Lesser
|
||||||
|
General Public License, and the "GNU GPL" refers to version 3 of the
|
||||||
|
GNU General Public License.
|
||||||
|
|
||||||
|
"The Library" refers to a covered work governed by this License, other
|
||||||
|
than an Application or a Combined Work as defined below.
|
||||||
|
|
||||||
|
An "Application" is any work that makes use of an interface provided
|
||||||
|
by the Library, but which is not otherwise based on the Library.
|
||||||
|
Defining a subclass of a class defined by the Library is deemed a mode
|
||||||
|
of using an interface provided by the Library.
|
||||||
|
|
||||||
|
A "Combined Work" is a work produced by combining or linking an
|
||||||
|
Application with the Library. The particular version of the Library
|
||||||
|
with which the Combined Work was made is also called the "Linked
|
||||||
|
Version".
|
||||||
|
|
||||||
|
The "Minimal Corresponding Source" for a Combined Work means the
|
||||||
|
Corresponding Source for the Combined Work, excluding any source code
|
||||||
|
for portions of the Combined Work that, considered in isolation, are
|
||||||
|
based on the Application, and not on the Linked Version.
|
||||||
|
|
||||||
|
The "Corresponding Application Code" for a Combined Work means the
|
||||||
|
object code and/or source code for the Application, including any data
|
||||||
|
and utility programs needed for reproducing the Combined Work from the
|
||||||
|
Application, but excluding the System Libraries of the Combined Work.
|
||||||
|
|
||||||
|
#### 1. Exception to Section 3 of the GNU GPL.
|
||||||
|
|
||||||
|
You may convey a covered work under sections 3 and 4 of this License
|
||||||
|
without being bound by section 3 of the GNU GPL.
|
||||||
|
|
||||||
|
#### 2. Conveying Modified Versions.
|
||||||
|
|
||||||
|
If you modify a copy of the Library, and, in your modifications, a
|
||||||
|
facility refers to a function or data to be supplied by an Application
|
||||||
|
that uses the facility (other than as an argument passed when the
|
||||||
|
facility is invoked), then you may convey a copy of the modified
|
||||||
|
version:
|
||||||
|
|
||||||
|
- a) under this License, provided that you make a good faith effort
|
||||||
|
to ensure that, in the event an Application does not supply the
|
||||||
|
function or data, the facility still operates, and performs
|
||||||
|
whatever part of its purpose remains meaningful, or
|
||||||
|
- b) under the GNU GPL, with none of the additional permissions of
|
||||||
|
this License applicable to that copy.
|
||||||
|
|
||||||
|
#### 3. Object Code Incorporating Material from Library Header Files.
|
||||||
|
|
||||||
|
The object code form of an Application may incorporate material from a
|
||||||
|
header file that is part of the Library. You may convey such object
|
||||||
|
code under terms of your choice, provided that, if the incorporated
|
||||||
|
material is not limited to numerical parameters, data structure
|
||||||
|
layouts and accessors, or small macros, inline functions and templates
|
||||||
|
(ten or fewer lines in length), you do both of the following:
|
||||||
|
|
||||||
|
- a) Give prominent notice with each copy of the object code that
|
||||||
|
the Library is used in it and that the Library and its use are
|
||||||
|
covered by this License.
|
||||||
|
- b) Accompany the object code with a copy of the GNU GPL and this
|
||||||
|
license document.
|
||||||
|
|
||||||
|
#### 4. Combined Works.
|
||||||
|
|
||||||
|
You may convey a Combined Work under terms of your choice that, taken
|
||||||
|
together, effectively do not restrict modification of the portions of
|
||||||
|
the Library contained in the Combined Work and reverse engineering for
|
||||||
|
debugging such modifications, if you also do each of the following:
|
||||||
|
|
||||||
|
- a) Give prominent notice with each copy of the Combined Work that
|
||||||
|
the Library is used in it and that the Library and its use are
|
||||||
|
covered by this License.
|
||||||
|
- b) Accompany the Combined Work with a copy of the GNU GPL and this
|
||||||
|
license document.
|
||||||
|
- c) For a Combined Work that displays copyright notices during
|
||||||
|
execution, include the copyright notice for the Library among
|
||||||
|
these notices, as well as a reference directing the user to the
|
||||||
|
copies of the GNU GPL and this license document.
|
||||||
|
- d) Do one of the following:
|
||||||
|
- 0) Convey the Minimal Corresponding Source under the terms of
|
||||||
|
this License, and the Corresponding Application Code in a form
|
||||||
|
suitable for, and under terms that permit, the user to
|
||||||
|
recombine or relink the Application with a modified version of
|
||||||
|
the Linked Version to produce a modified Combined Work, in the
|
||||||
|
manner specified by section 6 of the GNU GPL for conveying
|
||||||
|
Corresponding Source.
|
||||||
|
- 1) Use a suitable shared library mechanism for linking with
|
||||||
|
the Library. A suitable mechanism is one that (a) uses at run
|
||||||
|
time a copy of the Library already present on the user's
|
||||||
|
computer system, and (b) will operate properly with a modified
|
||||||
|
version of the Library that is interface-compatible with the
|
||||||
|
Linked Version.
|
||||||
|
- e) Provide Installation Information, but only if you would
|
||||||
|
otherwise be required to provide such information under section 6
|
||||||
|
of the GNU GPL, and only to the extent that such information is
|
||||||
|
necessary to install and execute a modified version of the
|
||||||
|
Combined Work produced by recombining or relinking the Application
|
||||||
|
with a modified version of the Linked Version. (If you use option
|
||||||
|
4d0, the Installation Information must accompany the Minimal
|
||||||
|
Corresponding Source and Corresponding Application Code. If you
|
||||||
|
use option 4d1, you must provide the Installation Information in
|
||||||
|
the manner specified by section 6 of the GNU GPL for conveying
|
||||||
|
Corresponding Source.)
|
||||||
|
|
||||||
|
#### 5. Combined Libraries.
|
||||||
|
|
||||||
|
You may place library facilities that are a work based on the Library
|
||||||
|
side by side in a single library together with other library
|
||||||
|
facilities that are not Applications and are not covered by this
|
||||||
|
License, and convey such a combined library under terms of your
|
||||||
|
choice, if you do both of the following:
|
||||||
|
|
||||||
|
- a) Accompany the combined library with a copy of the same work
|
||||||
|
based on the Library, uncombined with any other library
|
||||||
|
facilities, conveyed under the terms of this License.
|
||||||
|
- b) Give prominent notice with the combined library that part of it
|
||||||
|
is a work based on the Library, and explaining where to find the
|
||||||
|
accompanying uncombined form of the same work.
|
||||||
|
|
||||||
|
#### 6. Revised Versions of the GNU Lesser General Public License.
|
||||||
|
|
||||||
|
The Free Software Foundation may publish revised and/or new versions
|
||||||
|
of the GNU Lesser General Public License from time to time. Such new
|
||||||
|
versions will be similar in spirit to the present version, but may
|
||||||
|
differ in detail to address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the Library
|
||||||
|
as you received it specifies that a certain numbered version of the
|
||||||
|
GNU Lesser General Public License "or any later version" applies to
|
||||||
|
it, you have the option of following the terms and conditions either
|
||||||
|
of that published version or of any later version published by the
|
||||||
|
Free Software Foundation. If the Library as you received it does not
|
||||||
|
specify a version number of the GNU Lesser General Public License, you
|
||||||
|
may choose any version of the GNU Lesser General Public License ever
|
||||||
|
published by the Free Software Foundation.
|
||||||
|
|
||||||
|
If the Library as you received it specifies that a proxy can decide
|
||||||
|
whether future versions of the GNU Lesser General Public License shall
|
||||||
|
apply, that proxy's public statement of acceptance of any version is
|
||||||
|
permanent authorization for you to choose that version for the
|
||||||
|
Library.
|
159
README.md
Normal file
159
README.md
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
# flow
|
||||||
|
|
||||||
|
An experimental layout manager and formspec API replacement for Minetest.
|
||||||
|
Vaguely inspired by Flutter and GTK.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- No manual positioning of elements.
|
||||||
|
- Some elements have an automatic size.
|
||||||
|
- The size of elements can optionally expand to fit larger spaces
|
||||||
|
- No form names. Form names are still used internally, however they are hidden from the API.
|
||||||
|
- No having to worry about state.
|
||||||
|
- Values of fields, scrollbars, checkboxes, etc are remembered when redrawing
|
||||||
|
a formspec and are automatically applied.
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
|
||||||
|
- This mod doesn't support all of the features that regular formspecs do.
|
||||||
|
- [FS51](https://content.minetest.net/packages/luk3yx/fs51/) is required if
|
||||||
|
you want to have full support for Minetest 5.3 and below.
|
||||||
|
|
||||||
|
## Basic example
|
||||||
|
|
||||||
|
See `example.lua` for a more comprehensive example which demonstrates how
|
||||||
|
layouting and alignment works.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- GUI elements are accessible with flow.widgets. Using
|
||||||
|
-- `local gui = flow.widgets` is recommended to reduce typing.
|
||||||
|
local gui = flow.widgets
|
||||||
|
|
||||||
|
-- GUIs are created with flow.make_gui(build_func).
|
||||||
|
local my_gui = flow.make_gui(function(player, ctx)
|
||||||
|
-- The build function should return a GUI element such as gui.VBox.
|
||||||
|
-- `ctx` can be used to store context. `ctx.form` is reserved for storing
|
||||||
|
-- the state of elements in the form. For example, you can use
|
||||||
|
-- `ctx.form.my_checkbox` to check whether `my_checkbox` is checked. Note
|
||||||
|
-- that ctx.form.element may be nil instead of its default value.
|
||||||
|
|
||||||
|
-- This function may be called at any time by flow.
|
||||||
|
|
||||||
|
-- gui.VBox is a "container element" added by this mod.
|
||||||
|
return gui.VBox {
|
||||||
|
-- GUI elements have
|
||||||
|
gui.Label {label = "Here is a dropdown:"},
|
||||||
|
gui.Dropdown {
|
||||||
|
-- The value of this dropdown will be accessible from ctx.form.my_dropdown
|
||||||
|
name = "my_dropdown",
|
||||||
|
items = {'First item', 'Second item', 'Third item'},
|
||||||
|
index_event = true,
|
||||||
|
},
|
||||||
|
gui.Button {
|
||||||
|
label = "Get dropdown index",
|
||||||
|
on_event = function(player, ctx)
|
||||||
|
-- flow should guarantee that `ctx.form.my_dropdown` exists, even if the client doesn't send my_dropdown to the server.
|
||||||
|
local selected_idx = ctx.form.my_dropdown
|
||||||
|
minetest.chat_send_player(player:get_player_name(), "You have selected item #" .. selected_idx .. "!")
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- Show the GUI to player as an interactive form
|
||||||
|
-- Note that `player` is a player object and not a player name.
|
||||||
|
my_gui:show(player)
|
||||||
|
|
||||||
|
-- Close the form
|
||||||
|
my_gui:close(player)
|
||||||
|
|
||||||
|
-- Alternatively, the GUI can be shown as a non-interactive HUD (requires
|
||||||
|
-- hud_fs to be installed).
|
||||||
|
my_gui:show_hud(player)
|
||||||
|
my_gui:close_hud(player)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Other formspec libraries/utilities
|
||||||
|
|
||||||
|
These utilities likely aren't compatible with flow.
|
||||||
|
|
||||||
|
- [fs_layout](https://github.com/fluxionary/minetest-fs_layout/) is another mod library that does automatic formspec element positioning.
|
||||||
|
- [Just_Visiting's formspec editor](https://content.minetest.net/packages/Just_Visiting/formspec_editor) is a Minetest (sub)game that lets you edit formspecs and preview them as you go
|
||||||
|
- [kuto](https://github.com/TerraQuest-Studios/kuto/) is a formspec library that has some extra widgets/components and has a callback API. Some automatic sizing can be done for buttons.
|
||||||
|
- It may be possible to use kuto's components with flow somehow as they both use formspec_ast internally.
|
||||||
|
- [My web-based formspec editor](https://forum.minetest.net/viewtopic.php?f=14&t=24130) lets you add elements and drag+drop them, however it doesn't support all formspec features.
|
||||||
|
|
||||||
|
## Elements
|
||||||
|
|
||||||
|
You should do `local gui = flow.widgets` in your code.
|
||||||
|
|
||||||
|
### Layouting elements
|
||||||
|
|
||||||
|
These elements are used to lay out elements in the formspec. They don't have a
|
||||||
|
direct equivalent in Minetest formspecs.
|
||||||
|
|
||||||
|
#### `gui.VBox`
|
||||||
|
|
||||||
|
A vertical box, similar to a VBox in GTK. Elements in the VBox are stacked
|
||||||
|
vertically.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
gui.VBox{
|
||||||
|
-- These elements are documented later on.
|
||||||
|
gui.Label{label="I am a label!"},
|
||||||
|
|
||||||
|
-- The second label will be positioned underneath the first one.
|
||||||
|
gui.Label{label="I am a second label!"},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `gui.HBox`
|
||||||
|
|
||||||
|
Like `gui.VBox` but stacks elements horizontally instead.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
gui.HBox{
|
||||||
|
-- These elements are documented later on.
|
||||||
|
gui.Label{label="I am a label!"},
|
||||||
|
|
||||||
|
-- The second label will be positioned to the right of first one.
|
||||||
|
gui.Label{label="I am a second label!"},
|
||||||
|
|
||||||
|
-- You can nest HBox and VBox elements
|
||||||
|
gui.VBox{
|
||||||
|
gui.Image{texture_name="default_dirt.png", align_h = "centre"},
|
||||||
|
gui.Label{label="This label should be below the above texture."},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `gui.ScrollableVBox`
|
||||||
|
|
||||||
|
Similar to `gui.VBox` but uses a scroll_container and automatically adds a
|
||||||
|
scrollbar. You must specify a width and height for the scroll container.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
gui.ScrollableVBox{
|
||||||
|
-- A name must be provided for ScrollableVBox elements. You don't
|
||||||
|
-- have to use this name anywhere else, it just makes sure flow
|
||||||
|
-- doesn't mix up scrollbar states if one gets removed or if the
|
||||||
|
-- order changes.
|
||||||
|
name = "vbox1",
|
||||||
|
|
||||||
|
-- Specifying a height is optional but is probably a good idea.
|
||||||
|
-- If you don't specify a height, it will default to
|
||||||
|
-- min(height_of_content, 5).
|
||||||
|
h = 10,
|
||||||
|
|
||||||
|
-- These elements are documented later on.
|
||||||
|
gui.Label{label="I am a label!"},
|
||||||
|
|
||||||
|
-- The second label will be positioned underneath the first one.
|
||||||
|
gui.Label{label="I am a second label!"},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Minetest formspec elements
|
||||||
|
|
||||||
|
There is an auto-generated `elements.md` file which contains a list of elements
|
||||||
|
and parameters. Elements in this list haven't been tested and might not work.
|
418
elements.md
Normal file
418
elements.md
Normal file
@ -0,0 +1,418 @@
|
|||||||
|
# Auto-generated elements list
|
||||||
|
|
||||||
|
This is probably broken.
|
||||||
|
|
||||||
|
### `gui.AnimatedImage`
|
||||||
|
|
||||||
|
Equivalent to Minetest's `animated_image[]` element.
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
```lua
|
||||||
|
gui.AnimatedImage {
|
||||||
|
w = 1, -- Optional
|
||||||
|
h = 2, -- Optional
|
||||||
|
name = "my_animated_image", -- Optional
|
||||||
|
texture_name = "Hello world!",
|
||||||
|
frame_count = 3,
|
||||||
|
frame_duration = 4,
|
||||||
|
frame_start = 5, -- Optional
|
||||||
|
middle_x = 6, -- Optional
|
||||||
|
middle_y = 7, -- Optional
|
||||||
|
middle_x2 = 8, -- Optional
|
||||||
|
middle_y2 = 9, -- Optional
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `gui.Background`
|
||||||
|
|
||||||
|
Equivalent to Minetest's `background[]` element.
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
```lua
|
||||||
|
gui.Background {
|
||||||
|
w = 1, -- Optional
|
||||||
|
h = 2, -- Optional
|
||||||
|
texture_name = "Hello world!",
|
||||||
|
auto_clip = false, -- Optional
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `gui.Background9`
|
||||||
|
|
||||||
|
Equivalent to Minetest's `background9[]` element.
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
```lua
|
||||||
|
gui.Background9 {
|
||||||
|
w = 1, -- Optional
|
||||||
|
h = 2, -- Optional
|
||||||
|
texture_name = "Hello world!",
|
||||||
|
auto_clip = false,
|
||||||
|
middle_x = 3,
|
||||||
|
middle_y = 4, -- Optional
|
||||||
|
middle_x2 = 5, -- Optional
|
||||||
|
middle_y2 = 6, -- Optional
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `gui.Box`
|
||||||
|
|
||||||
|
Equivalent to Minetest's `box[]` element.
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
```lua
|
||||||
|
gui.Box {
|
||||||
|
w = 1, -- Optional
|
||||||
|
h = 2, -- Optional
|
||||||
|
color = "#FF0000",
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `gui.Button`
|
||||||
|
|
||||||
|
Equivalent to Minetest's `button[]` element.
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
```lua
|
||||||
|
gui.Button {
|
||||||
|
w = 1, -- Optional
|
||||||
|
h = 2, -- Optional
|
||||||
|
name = "my_button", -- Optional
|
||||||
|
label = "Hello world!",
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `gui.ButtonExit`
|
||||||
|
|
||||||
|
Equivalent to Minetest's `button_exit[]` element.
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
```lua
|
||||||
|
gui.ButtonExit {
|
||||||
|
w = 1, -- Optional
|
||||||
|
h = 2, -- Optional
|
||||||
|
name = "my_button_exit", -- Optional
|
||||||
|
label = "Hello world!",
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `gui.Checkbox`
|
||||||
|
|
||||||
|
Equivalent to Minetest's `checkbox[]` element.
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
```lua
|
||||||
|
gui.Checkbox {
|
||||||
|
name = "my_checkbox", -- Optional
|
||||||
|
label = "Hello world!",
|
||||||
|
selected = false, -- Optional
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `gui.Dropdown`
|
||||||
|
|
||||||
|
Equivalent to Minetest's `dropdown[]` element.
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
```lua
|
||||||
|
gui.Dropdown {
|
||||||
|
w = 1, -- Optional
|
||||||
|
h = 2, -- Optional
|
||||||
|
name = "my_dropdown", -- Optional
|
||||||
|
items = "Hello world!",
|
||||||
|
selected_idx = 3,
|
||||||
|
index_event = false, -- Optional
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `gui.Field`
|
||||||
|
|
||||||
|
Equivalent to Minetest's `field[]` element.
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
```lua
|
||||||
|
gui.Field {
|
||||||
|
w = 1, -- Optional
|
||||||
|
h = 2, -- Optional
|
||||||
|
name = "my_field", -- Optional
|
||||||
|
label = "Hello world!",
|
||||||
|
default = "Hello world!",
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `gui.Hypertext`
|
||||||
|
|
||||||
|
Equivalent to Minetest's `hypertext[]` element.
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
```lua
|
||||||
|
gui.Hypertext {
|
||||||
|
w = 1, -- Optional
|
||||||
|
h = 2, -- Optional
|
||||||
|
name = "my_hypertext", -- Optional
|
||||||
|
text = "Hello world!",
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `gui.Image`
|
||||||
|
|
||||||
|
Equivalent to Minetest's `image[]` element.
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
```lua
|
||||||
|
gui.Image {
|
||||||
|
w = 1, -- Optional
|
||||||
|
h = 2, -- Optional
|
||||||
|
texture_name = "Hello world!",
|
||||||
|
middle_x = 3, -- Optional
|
||||||
|
middle_y = 4, -- Optional
|
||||||
|
middle_x2 = 5, -- Optional
|
||||||
|
middle_y2 = 6, -- Optional
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `gui.ImageButton`
|
||||||
|
|
||||||
|
Equivalent to Minetest's `image_button[]` element.
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
```lua
|
||||||
|
gui.ImageButton {
|
||||||
|
w = 1, -- Optional
|
||||||
|
h = 2, -- Optional
|
||||||
|
texture_name = "Hello world!",
|
||||||
|
name = "my_image_button", -- Optional
|
||||||
|
label = "Hello world!",
|
||||||
|
noclip = false, -- Optional
|
||||||
|
drawborder = false, -- Optional
|
||||||
|
pressed_texture_name = "Hello world!", -- Optional
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `gui.ImageButtonExit`
|
||||||
|
|
||||||
|
Equivalent to Minetest's `image_button_exit[]` element.
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
```lua
|
||||||
|
gui.ImageButtonExit {
|
||||||
|
w = 1, -- Optional
|
||||||
|
h = 2, -- Optional
|
||||||
|
texture_name = "Hello world!",
|
||||||
|
name = "my_image_button_exit", -- Optional
|
||||||
|
label = "Hello world!",
|
||||||
|
noclip = false, -- Optional
|
||||||
|
drawborder = false, -- Optional
|
||||||
|
pressed_texture_name = "Hello world!", -- Optional
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `gui.ItemImage`
|
||||||
|
|
||||||
|
Equivalent to Minetest's `item_image[]` element.
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
```lua
|
||||||
|
gui.ItemImage {
|
||||||
|
w = 1, -- Optional
|
||||||
|
h = 2, -- Optional
|
||||||
|
item_name = "Hello world!",
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `gui.ItemImageButton`
|
||||||
|
|
||||||
|
Equivalent to Minetest's `item_image_button[]` element.
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
```lua
|
||||||
|
gui.ItemImageButton {
|
||||||
|
w = 1, -- Optional
|
||||||
|
h = 2, -- Optional
|
||||||
|
item_name = "Hello world!",
|
||||||
|
name = "my_item_image_button", -- Optional
|
||||||
|
label = "Hello world!",
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `gui.Label`
|
||||||
|
|
||||||
|
Equivalent to Minetest's `label[]` element.
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
```lua
|
||||||
|
gui.Label {
|
||||||
|
label = "Hello world!",
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `gui.List`
|
||||||
|
|
||||||
|
Equivalent to Minetest's `list[]` element.
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
```lua
|
||||||
|
gui.List {
|
||||||
|
inventory_location = "Hello world!",
|
||||||
|
list_name = "Hello world!",
|
||||||
|
w = 1,
|
||||||
|
h = 2,
|
||||||
|
starting_item_index = 3, -- Optional
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `gui.Model`
|
||||||
|
|
||||||
|
Equivalent to Minetest's `model[]` element.
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
```lua
|
||||||
|
gui.Model {
|
||||||
|
w = 1, -- Optional
|
||||||
|
h = 2, -- Optional
|
||||||
|
name = "my_model", -- Optional
|
||||||
|
mesh = "Hello world!",
|
||||||
|
textures = "Hello world!",
|
||||||
|
rotation_x = 3, -- Optional
|
||||||
|
rotation_y = 4, -- Optional
|
||||||
|
continuous = false, -- Optional
|
||||||
|
mouse_control = false, -- Optional
|
||||||
|
frame_loop_begin = 5, -- Optional
|
||||||
|
frame_loop_end = 6, -- Optional
|
||||||
|
animation_speed = 7, -- Optional
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `gui.Pwdfield`
|
||||||
|
|
||||||
|
Equivalent to Minetest's `pwdfield[]` element.
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
```lua
|
||||||
|
gui.Pwdfield {
|
||||||
|
w = 1, -- Optional
|
||||||
|
h = 2, -- Optional
|
||||||
|
name = "my_pwdfield", -- Optional
|
||||||
|
label = "Hello world!",
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `gui.ScrollContainer`
|
||||||
|
|
||||||
|
Equivalent to Minetest's `scroll_container[]` element.
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
```lua
|
||||||
|
gui.ScrollContainer {
|
||||||
|
w = 1, -- Optional
|
||||||
|
h = 2, -- Optional
|
||||||
|
scrollbar_name = "Hello world!",
|
||||||
|
orientation = "vertical",
|
||||||
|
scroll_factor = 3, -- Optional
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `gui.Scrollbar`
|
||||||
|
|
||||||
|
Equivalent to Minetest's `scrollbar[]` element.
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
```lua
|
||||||
|
gui.Scrollbar {
|
||||||
|
w = 1, -- Optional
|
||||||
|
h = 2, -- Optional
|
||||||
|
orientation = "vertical",
|
||||||
|
name = "my_scrollbar", -- Optional
|
||||||
|
value = 3,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `gui.Tabheader`
|
||||||
|
|
||||||
|
Equivalent to Minetest's `tabheader[]` element.
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
```lua
|
||||||
|
gui.Tabheader {
|
||||||
|
h = 1, -- Optional
|
||||||
|
name = "my_tabheader", -- Optional
|
||||||
|
captions = "Hello world!",
|
||||||
|
current_tab = "Hello world!",
|
||||||
|
transparent = false, -- Optional
|
||||||
|
draw_border = false, -- Optional
|
||||||
|
w = 2, -- Optional
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `gui.Table`
|
||||||
|
|
||||||
|
Equivalent to Minetest's `table[]` element.
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
```lua
|
||||||
|
gui.Table {
|
||||||
|
w = 1, -- Optional
|
||||||
|
h = 2, -- Optional
|
||||||
|
name = "my_table", -- Optional
|
||||||
|
cells = "Hello world!",
|
||||||
|
selected_idx = 3,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `gui.Textarea`
|
||||||
|
|
||||||
|
Equivalent to Minetest's `textarea[]` element.
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
```lua
|
||||||
|
gui.Textarea {
|
||||||
|
w = 1, -- Optional
|
||||||
|
h = 2, -- Optional
|
||||||
|
name = "my_textarea", -- Optional
|
||||||
|
label = "Hello world!",
|
||||||
|
default = "Hello world!",
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `gui.Textlist`
|
||||||
|
|
||||||
|
Equivalent to Minetest's `textlist[]` element.
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
```lua
|
||||||
|
gui.Textlist {
|
||||||
|
w = 1, -- Optional
|
||||||
|
h = 2, -- Optional
|
||||||
|
name = "my_textlist", -- Optional
|
||||||
|
listelems = "Hello world!",
|
||||||
|
selected_idx = 3, -- Optional
|
||||||
|
transparent = false, -- Optional
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `gui.Tooltip`
|
||||||
|
|
||||||
|
Equivalent to Minetest's `tooltip[]` element.
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
```lua
|
||||||
|
gui.Tooltip {
|
||||||
|
w = 1, -- Optional
|
||||||
|
h = 2, -- Optional
|
||||||
|
tooltip_text = "Hello world!",
|
||||||
|
bgcolor = "#FF0000", -- Optional
|
||||||
|
fontcolor = "#FF0000", -- Optional
|
||||||
|
gui_element_name = "Hello world!", -- Optional
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `gui.Vertlabel`
|
||||||
|
|
||||||
|
Equivalent to Minetest's `vertlabel[]` element.
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
```lua
|
||||||
|
gui.Vertlabel {
|
||||||
|
label = "Hello world!",
|
||||||
|
}
|
||||||
|
```
|
208
example.lua
Normal file
208
example.lua
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
-- Debugging
|
||||||
|
local gui = flow.widgets
|
||||||
|
|
||||||
|
local elements = {"box", "label", "image", "field", "checkbox", "list"}
|
||||||
|
local alignments = {"auto", "start", "end", "centre", "fill"}
|
||||||
|
|
||||||
|
local my_gui = flow.make_gui(function(player, ctx)
|
||||||
|
local hbox = {
|
||||||
|
min_h = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
local elem_type = elements[ctx.form.element] or "box"
|
||||||
|
|
||||||
|
-- Setting a width/height on labels, fields, or checkboxes can break things
|
||||||
|
local w, h
|
||||||
|
if elem_type ~= "label" and elem_type ~= "field" and
|
||||||
|
elem_type ~= "checkbox" then
|
||||||
|
w, h = 1, 1
|
||||||
|
end
|
||||||
|
|
||||||
|
hbox[#hbox + 1] = {
|
||||||
|
type = elem_type,
|
||||||
|
w = w,
|
||||||
|
h = h,
|
||||||
|
label = "Label",
|
||||||
|
color = "#fff",
|
||||||
|
texture_name = "air.png",
|
||||||
|
|
||||||
|
expand = ctx.form.expand,
|
||||||
|
align_h = alignments[ctx.form.align_h],
|
||||||
|
align_v = alignments[ctx.form.align_v],
|
||||||
|
name = "testing",
|
||||||
|
|
||||||
|
inventory_location = "current_player",
|
||||||
|
list_name = "main",
|
||||||
|
}
|
||||||
|
|
||||||
|
if ctx.form.box2 then
|
||||||
|
hbox[#hbox + 1] = gui.Box{
|
||||||
|
w = 1,
|
||||||
|
h = 1,
|
||||||
|
color = "#888",
|
||||||
|
expand = ctx.form.expand_box2,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local try_it_yourself_box
|
||||||
|
if ctx.form.vbox then
|
||||||
|
try_it_yourself_box = gui.VBox(hbox)
|
||||||
|
else
|
||||||
|
try_it_yourself_box = gui.HBox(hbox)
|
||||||
|
end
|
||||||
|
|
||||||
|
return gui.VBox{
|
||||||
|
-- Optionally specify a minimum size for the form
|
||||||
|
min_w = 8,
|
||||||
|
min_h = 9,
|
||||||
|
|
||||||
|
gui.HBox{
|
||||||
|
gui.Image{w = 1, h = 1, texture_name = "air.png"},
|
||||||
|
gui.Label{label = "Hello world!"},
|
||||||
|
},
|
||||||
|
gui.Label{label="This is an example form."},
|
||||||
|
gui.Checkbox{
|
||||||
|
name = "checkbox",
|
||||||
|
|
||||||
|
-- flow will detect that you have accessed ctx.form.checkbox and
|
||||||
|
-- will automatically redraw the formspec if the value is changed.
|
||||||
|
label = ctx.form.checkbox and "Uncheck me!" or "Check me!",
|
||||||
|
},
|
||||||
|
gui.Button{
|
||||||
|
-- Names are optional
|
||||||
|
label = "Toggle checkbox",
|
||||||
|
|
||||||
|
-- Important: Do not use the `player` and `ctx` variables from the
|
||||||
|
-- above formspec.
|
||||||
|
on_event = function(player, ctx)
|
||||||
|
-- Invert the value of the checkbox
|
||||||
|
ctx.form.checkbox = not ctx.form.checkbox
|
||||||
|
|
||||||
|
-- Send a chat message
|
||||||
|
minetest.chat_send_player(player:get_player_name(), "Toggled!")
|
||||||
|
|
||||||
|
-- Return true to tell flow to redraw the formspec
|
||||||
|
return true
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
|
||||||
|
gui.Label{label="A demonstration of expansion:"},
|
||||||
|
|
||||||
|
-- The finer details of scroll containers are handled automatically.
|
||||||
|
-- Clients that don't support scroll_container[] will see a paginator
|
||||||
|
-- instead.
|
||||||
|
gui.ScrollableVBox{
|
||||||
|
-- A name must be provided for ScrollableVBox elements. You don't
|
||||||
|
-- have to use this name anywhere else, it just makes sure flow
|
||||||
|
-- doesn't mix up scrollbar states if one gets removed or if the
|
||||||
|
-- order changes.
|
||||||
|
name = "vbox1",
|
||||||
|
|
||||||
|
gui.Label{label="By default, objects do not expand\nin the " ..
|
||||||
|
"same direction as the hbox/vbox:"},
|
||||||
|
gui.HBox{
|
||||||
|
gui.Box{
|
||||||
|
w = 1,
|
||||||
|
h = 1,
|
||||||
|
color = "#fff",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
gui.Label{label="Items are expanded in the opposite\ndirection," ..
|
||||||
|
" however:"},
|
||||||
|
gui.HBox{
|
||||||
|
min_h = 2,
|
||||||
|
gui.Box{
|
||||||
|
w = 1,
|
||||||
|
h = 1,
|
||||||
|
color = "#fff",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
gui.Label{label="To automatically expand an object, add\n" ..
|
||||||
|
"`expand = true` to its definition."},
|
||||||
|
gui.HBox{
|
||||||
|
gui.Box{
|
||||||
|
w = 1,
|
||||||
|
h = 1,
|
||||||
|
color = "#fff",
|
||||||
|
expand = true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
gui.Label{label="Multiple expanded items will share the\n" ..
|
||||||
|
"remaining space evenly."},
|
||||||
|
|
||||||
|
gui.HBox{
|
||||||
|
gui.Box{
|
||||||
|
w = 1,
|
||||||
|
h = 1,
|
||||||
|
color = "#fff",
|
||||||
|
expand = true
|
||||||
|
},
|
||||||
|
gui.Box{
|
||||||
|
w = 1,
|
||||||
|
h = 1,
|
||||||
|
color = "#fff",
|
||||||
|
expand = true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
gui.HBox{
|
||||||
|
gui.Box{
|
||||||
|
w = 1,
|
||||||
|
h = 1,
|
||||||
|
color = "#fff",
|
||||||
|
expand = true
|
||||||
|
},
|
||||||
|
gui.Box{
|
||||||
|
w = 3,
|
||||||
|
h = 1,
|
||||||
|
color = "#fff",
|
||||||
|
expand = true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
gui.Label{label="Try it yourself!"},
|
||||||
|
gui.HBox{
|
||||||
|
gui.VBox{
|
||||||
|
gui.Label{label="Element:"},
|
||||||
|
gui.Dropdown{
|
||||||
|
name = "element",
|
||||||
|
items = elements,
|
||||||
|
index_event = true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
gui.VBox{
|
||||||
|
gui.Label{label="align_h:"},
|
||||||
|
gui.Dropdown{
|
||||||
|
name = "align_h",
|
||||||
|
items = {"auto (default)", "start / top / left",
|
||||||
|
"end / bottom / right", "centre / center", "fill"},
|
||||||
|
index_event = true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
gui.VBox{
|
||||||
|
gui.Label{label="align_v:"},
|
||||||
|
gui.Dropdown{
|
||||||
|
name = "align_v",
|
||||||
|
items = {"auto (default)", "start / top / left",
|
||||||
|
"end / bottom / right", "centre / center", "fill"},
|
||||||
|
index_event = true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
gui.HBox{
|
||||||
|
gui.Checkbox{name = "expand", label = "Expand"},
|
||||||
|
gui.Checkbox{name = "vbox", label = "Use vbox instead of hbox"},
|
||||||
|
},
|
||||||
|
gui.HBox{
|
||||||
|
gui.Checkbox{name = "box2", label = "Second box"},
|
||||||
|
gui.Checkbox{name = "expand_box2", label = "Expand second box"},
|
||||||
|
},
|
||||||
|
try_it_yourself_box,
|
||||||
|
}
|
||||||
|
end)
|
||||||
|
|
||||||
|
return my_gui
|
91
generate_docs.py
Normal file
91
generate_docs.py
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
from ruamel.yaml import YAML
|
||||||
|
import collections, re, requests
|
||||||
|
yaml = YAML(typ='safe')
|
||||||
|
|
||||||
|
def fetch_elements():
|
||||||
|
res = requests.get('https://github.com/luk3yx/minetest-formspec_ast/raw/'
|
||||||
|
'master/elements.yaml')
|
||||||
|
return yaml.load(res.text)
|
||||||
|
|
||||||
|
|
||||||
|
def search_for_fields(obj):
|
||||||
|
assert isinstance(obj, (list, tuple))
|
||||||
|
if len(obj) == 2:
|
||||||
|
if obj[1] == '...':
|
||||||
|
yield from search_for_fields(obj[0])
|
||||||
|
return
|
||||||
|
if isinstance(obj[0], str) and isinstance(obj[1], str):
|
||||||
|
yield tuple(obj)
|
||||||
|
return
|
||||||
|
|
||||||
|
for e in obj:
|
||||||
|
yield from search_for_fields(e)
|
||||||
|
|
||||||
|
|
||||||
|
def element_to_docs(element_name, variants):
|
||||||
|
flow_name = re.sub(r'_(.)', lambda m: m.group(1).upper(),
|
||||||
|
element_name.capitalize())
|
||||||
|
|
||||||
|
res = [
|
||||||
|
f'### `gui.{flow_name}`\n',
|
||||||
|
f"Equivalent to Minetest's `{element_name}[]` element.\n",
|
||||||
|
'**Example**',
|
||||||
|
'```lua',
|
||||||
|
f'gui.{flow_name} {{'
|
||||||
|
]
|
||||||
|
|
||||||
|
fields = collections.Counter(search_for_fields(variants))
|
||||||
|
if (('x', 'number') not in fields or
|
||||||
|
all(field_name in ('x', 'y') for field_name, _ in fields)):
|
||||||
|
return ''
|
||||||
|
|
||||||
|
num = 1
|
||||||
|
|
||||||
|
for (field_name, field_type), count in fields.items():
|
||||||
|
if field_name in ('x', 'y'):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if field_type == 'number':
|
||||||
|
value = num
|
||||||
|
num += 1
|
||||||
|
elif field_type == 'string':
|
||||||
|
if field_name == 'name':
|
||||||
|
value = f'"my_{element_name}"'
|
||||||
|
elif field_name == 'orientation':
|
||||||
|
value = '"vertical"'
|
||||||
|
elif 'color' in field_name:
|
||||||
|
value = '"#FF0000"'
|
||||||
|
else:
|
||||||
|
value = '"Hello world!"'
|
||||||
|
elif field_type in ('boolean', 'fullscreen'):
|
||||||
|
value = 'false'
|
||||||
|
elif field_type == 'table':
|
||||||
|
value = '{field = "value"}'
|
||||||
|
else:
|
||||||
|
value = '<?>'
|
||||||
|
|
||||||
|
line = f' {field_name} = {value},'
|
||||||
|
if ((field_name in ('name', 'w', 'h') and element_name != 'list') or
|
||||||
|
count < len(variants)):
|
||||||
|
line = line + ' -- Optional'
|
||||||
|
res.append(line)
|
||||||
|
|
||||||
|
res.append('}')
|
||||||
|
res.append('```')
|
||||||
|
|
||||||
|
return '\n'.join(res)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
print('Fetching data...')
|
||||||
|
elements = fetch_elements()
|
||||||
|
print('Done.')
|
||||||
|
|
||||||
|
with open('elements.md', 'w') as f:
|
||||||
|
f.write('# Auto-generated elements list\n\n')
|
||||||
|
f.write('This is probably broken.')
|
||||||
|
for element_name, variants in elements.items():
|
||||||
|
docs = element_to_docs(element_name, variants)
|
||||||
|
if docs:
|
||||||
|
f.write('\n\n')
|
||||||
|
f.write(docs)
|
926
init.lua
Normal file
926
init.lua
Normal file
@ -0,0 +1,926 @@
|
|||||||
|
--
|
||||||
|
-- Minetest 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 3 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
|
||||||
|
local hot_reload = (DEBUG_MODE and minetest.global_exists("flow") and
|
||||||
|
flow.hot_reload or {})
|
||||||
|
flow = {}
|
||||||
|
|
||||||
|
|
||||||
|
local Form = {}
|
||||||
|
|
||||||
|
local min, max = math.min, math.max
|
||||||
|
|
||||||
|
local function strip_escape_sequences(str)
|
||||||
|
return (str:gsub("\27%([^)]+%)", ""):gsub("\27.", ""))
|
||||||
|
end
|
||||||
|
|
||||||
|
local LABEL_HEIGHT = 0.4
|
||||||
|
local LABEL_OFFSET = LABEL_HEIGHT / 2
|
||||||
|
local CHARS_PER_UNIT = 4.8 -- 5
|
||||||
|
local function get_lines_size(lines)
|
||||||
|
local w = 0
|
||||||
|
for _, line in ipairs(lines) do
|
||||||
|
w = max(w, #strip_escape_sequences(line) / CHARS_PER_UNIT)
|
||||||
|
end
|
||||||
|
return w, LABEL_HEIGHT * #lines
|
||||||
|
end
|
||||||
|
|
||||||
|
local function get_label_size(label)
|
||||||
|
return get_lines_size((label or ""):split("\n", true))
|
||||||
|
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
|
||||||
|
|
||||||
|
function size_getters.field(node)
|
||||||
|
local label_w, label_h = get_label_size(node.label)
|
||||||
|
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 1 / CHARS_PER_UNIT, #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 function apply_padding(node, x, y, extra_padding)
|
||||||
|
local w, h = get_and_fill_in_sizes(node)
|
||||||
|
|
||||||
|
if extra_padding then
|
||||||
|
w = w + extra_padding
|
||||||
|
h = h + extra_padding
|
||||||
|
end
|
||||||
|
|
||||||
|
if node.type == "label" or node.type == "checkbox" then
|
||||||
|
y = y + LABEL_OFFSET
|
||||||
|
end
|
||||||
|
|
||||||
|
if node._padding_top then
|
||||||
|
y = y + node._padding_top
|
||||||
|
h = h + node._padding_top
|
||||||
|
end
|
||||||
|
|
||||||
|
if node.padding then
|
||||||
|
x = x + node.padding
|
||||||
|
y = y + node.padding
|
||||||
|
w = w + node.padding * 2
|
||||||
|
h = h + node.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,
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
-- Special cases
|
||||||
|
for _, node in ipairs(hbox) do
|
||||||
|
if node.type == "checkbox" then
|
||||||
|
node.y = height / 2
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return x, height
|
||||||
|
end
|
||||||
|
|
||||||
|
function size_getters.padding(node)
|
||||||
|
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" 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"
|
||||||
|
node[1] = {
|
||||||
|
type = "image_button",
|
||||||
|
texture_name = "blank.png",
|
||||||
|
drawborder = false,
|
||||||
|
x = 0, y = 0,
|
||||||
|
w = node.w + extra_space, h = node.h,
|
||||||
|
label = node.label,
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Overlay button to prevent clicks from doing anything
|
||||||
|
node[2] = {
|
||||||
|
type = "image_button",
|
||||||
|
texture_name = "blank.png",
|
||||||
|
drawborder = false,
|
||||||
|
x = 0, y = 0,
|
||||||
|
w = node.w + extra_space, h = node.h,
|
||||||
|
label = "",
|
||||||
|
}
|
||||||
|
|
||||||
|
node.y = node.y - LABEL_OFFSET
|
||||||
|
node.label = nil
|
||||||
|
assert(#node == 2)
|
||||||
|
end
|
||||||
|
node[w] = node[w] + extra_space
|
||||||
|
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 function expand(box)
|
||||||
|
local x, w, align_h, y, h, align_v
|
||||||
|
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 == "padding" then
|
||||||
|
box.type = "container"
|
||||||
|
local node = box[1]
|
||||||
|
if node.expand then
|
||||||
|
align_types[node.align_h or "auto"](node, "x", "w", box.w -
|
||||||
|
node.w - ((node.padding or 0) + (box.padding or 0)) * 2)
|
||||||
|
align_types[node.align_v or "auto"](node, "y", "h", box.h -
|
||||||
|
node.h - ((node.padding or 0) + (box.padding or 0)) * 2 -
|
||||||
|
(node._padding_top or 0) - (box._padding_top or 0))
|
||||||
|
end
|
||||||
|
return expand(node)
|
||||||
|
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
|
||||||
|
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
|
||||||
|
for i, node in ipairs(box) do
|
||||||
|
local width, height = node[w] or 0, node[h] or 0
|
||||||
|
if width > 0 and height > 0 then
|
||||||
|
if i > 1 then
|
||||||
|
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
|
||||||
|
|
||||||
|
if node.expand then
|
||||||
|
expandable[node] = i
|
||||||
|
expand_count = expand_count + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Nodes are expanded in the other direction no matter what their
|
||||||
|
-- expand setting is
|
||||||
|
if box_h > height and height > 0 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 - (node.padding or 0) * 2)
|
||||||
|
|
||||||
|
-- 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
|
||||||
|
local align = node.algin_h or "auto"
|
||||||
|
if align == "centre" or align == "center" or align == "fill" or
|
||||||
|
(align == "auto" and node.expand) then
|
||||||
|
align_types.fill(node, "x", "w", 0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Recursively expand
|
||||||
|
for _, node in ipairs(box) do
|
||||||
|
expand(node)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Renders the GUI into hopefully valid AST
|
||||||
|
-- This won't fill in names
|
||||||
|
local function render_ast(node)
|
||||||
|
local t1 = minetest.get_us_time()
|
||||||
|
local w, h = apply_padding(node, 0.3, 0.3, 0.6, 0.6)
|
||||||
|
local t2 = minetest.get_us_time()
|
||||||
|
expand(node)
|
||||||
|
local t3 = minetest.get_us_time()
|
||||||
|
local res = {
|
||||||
|
formspec_version = 5,
|
||||||
|
{type = "size", w = w, h = h},
|
||||||
|
}
|
||||||
|
for field in formspec_ast.find(node, 'field') do
|
||||||
|
res[#res + 1] = {
|
||||||
|
type = 'field_close_on_enter',
|
||||||
|
name = field.name,
|
||||||
|
close_on_enter = false,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
res[#res + 1] = node
|
||||||
|
local t4 = minetest.get_us_time()
|
||||||
|
print('apply_padding', t2 - t1)
|
||||||
|
print('expand', t3 - t2)
|
||||||
|
print('field_close_on_enter', t4 - t3)
|
||||||
|
return res
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Try and create short (2 byte) names
|
||||||
|
local function get_identifier(i)
|
||||||
|
if i > 127 then
|
||||||
|
-- Give up and use long (but unique) names
|
||||||
|
return '\1\1' .. tostring(i)
|
||||||
|
end
|
||||||
|
return string.char(1, i)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function chain_cb(f1, f2)
|
||||||
|
return function(...)
|
||||||
|
f1(...)
|
||||||
|
f2(...)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local field_value_transformers = {
|
||||||
|
tabheader = tonumber,
|
||||||
|
dropdown = tonumber,
|
||||||
|
checkbox = minetest.is_yes,
|
||||||
|
table = function(value)
|
||||||
|
return minetest.explode_table_event(value).row
|
||||||
|
end,
|
||||||
|
textlist = function(value)
|
||||||
|
return minetest.explode_textlist_event(value).index
|
||||||
|
end,
|
||||||
|
scrollbar = function(value)
|
||||||
|
return minetest.explode_scrollbar_event(value).value
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
|
||||||
|
local function default_field_value_transformer(value)
|
||||||
|
return value
|
||||||
|
end
|
||||||
|
|
||||||
|
local default_value_fields = {
|
||||||
|
field = "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 = 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Removes on_event from a formspec_ast tree and returns a callbacks table
|
||||||
|
local function parse_callbacks(tree, ctx_form)
|
||||||
|
local i = 0
|
||||||
|
local callbacks = {}
|
||||||
|
local saved_fields = {}
|
||||||
|
local seen_scroll_container = false
|
||||||
|
for node in formspec_ast.walk(tree) do
|
||||||
|
if node.type == "container" then
|
||||||
|
if node.bgcolor then
|
||||||
|
table.insert(node, 1, {
|
||||||
|
type = "box", color = node.bgcolor,
|
||||||
|
x = 0, y = 0, w = node.w, h = node.h,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
if node.bgimg then
|
||||||
|
table.insert(node, 1, {
|
||||||
|
type = "background", texture_name = node.bgimg,
|
||||||
|
x = 0, y = 0, w = node.w, h = node.h,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
if node.on_quit then
|
||||||
|
if callbacks.quit then
|
||||||
|
-- HACK
|
||||||
|
callbacks.quit = chain_cb(callbacks.quit, node.on_quit)
|
||||||
|
else
|
||||||
|
callbacks.quit = node.on_quit
|
||||||
|
end
|
||||||
|
end
|
||||||
|
elseif seen_scroll_container then
|
||||||
|
-- Work around a Minetest bug with scroll containers not scrolling
|
||||||
|
-- backgrounds.
|
||||||
|
if node.type == "background" and not node.auto_clip then
|
||||||
|
node.type = "image"
|
||||||
|
end
|
||||||
|
elseif node.type == "scroll_container" then
|
||||||
|
seen_scroll_container = true
|
||||||
|
end
|
||||||
|
|
||||||
|
local node_name = node.name
|
||||||
|
if node_name then
|
||||||
|
local value_field = default_value_fields[node.type]
|
||||||
|
if value_field then
|
||||||
|
-- Add the corresponding value transformer transformer to
|
||||||
|
-- saved_fields
|
||||||
|
saved_fields[node_name] = (
|
||||||
|
field_value_transformers[node.type] or
|
||||||
|
default_field_value_transformer
|
||||||
|
)
|
||||||
|
|
||||||
|
-- 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 then
|
||||||
|
-- Special case for dropdowns without index_event
|
||||||
|
if node.items then
|
||||||
|
if value == nil then
|
||||||
|
ctx_form[node_name] = node.items[
|
||||||
|
node.selected_idx or 1
|
||||||
|
]
|
||||||
|
else
|
||||||
|
local idx = table.indexof(node.items, value)
|
||||||
|
if idx > 0 then
|
||||||
|
node.selected_idx = idx
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
saved_fields[node_name] = default_field_value_transformer
|
||||||
|
elseif value == nil then
|
||||||
|
ctx_form[node_name] = node[value_field] or
|
||||||
|
sensible_defaults[value_field]
|
||||||
|
else
|
||||||
|
node[value_field] = value or sensible_defaults[value_field]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if node.on_event then
|
||||||
|
if not node_name then
|
||||||
|
i = i + 1
|
||||||
|
node_name = get_identifier(i)
|
||||||
|
node.name = node_name
|
||||||
|
end
|
||||||
|
|
||||||
|
callbacks[node_name] = node.on_event
|
||||||
|
node.on_event = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return callbacks, saved_fields
|
||||||
|
end
|
||||||
|
|
||||||
|
local gui = setmetatable({
|
||||||
|
embed = function(fs, w, h)
|
||||||
|
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,
|
||||||
|
}, {
|
||||||
|
__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,
|
||||||
|
__newindex = function()
|
||||||
|
error("Cannot modifiy gui table")
|
||||||
|
end
|
||||||
|
})
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
-- Renders a GUI into a formspec_ast tree and a table with callbacks.
|
||||||
|
function Form:_render(player, ctx, formspec_version)
|
||||||
|
local used_ctx_vars = {}
|
||||||
|
|
||||||
|
-- Wrap ctx.form
|
||||||
|
local orig_form = ctx.form or {}
|
||||||
|
local wrapped_form = setmetatable({}, {
|
||||||
|
__index = function(_, key)
|
||||||
|
used_ctx_vars[key] = true
|
||||||
|
return orig_form[key]
|
||||||
|
end,
|
||||||
|
__newindex = function(_, key, value)
|
||||||
|
orig_form[key] = value
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
ctx.form = wrapped_form
|
||||||
|
|
||||||
|
gui.formspec_version = formspec_version or 0
|
||||||
|
current_ctx = ctx
|
||||||
|
local box = self._build(player, ctx)
|
||||||
|
current_ctx = nil
|
||||||
|
gui.formspec_version = 0
|
||||||
|
|
||||||
|
-- Restore the original ctx.form
|
||||||
|
assert(ctx.form == wrapped_form,
|
||||||
|
"Changing the value of ctx.form is not supported!")
|
||||||
|
ctx.form = orig_form
|
||||||
|
|
||||||
|
local tree = render_ast(box)
|
||||||
|
local callbacks, saved_fields = parse_callbacks(tree, orig_form)
|
||||||
|
|
||||||
|
local redraw_if_changed = {}
|
||||||
|
for var in pairs(used_ctx_vars) do
|
||||||
|
-- Only add it if there is no callback and the name exists in the
|
||||||
|
-- formspec.
|
||||||
|
if saved_fields[var] and not callbacks[var] then
|
||||||
|
redraw_if_changed[var] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return tree, {
|
||||||
|
self = self,
|
||||||
|
formname = self._formname,
|
||||||
|
callbacks = callbacks,
|
||||||
|
saved_fields = saved_fields,
|
||||||
|
redraw_if_changed = redraw_if_changed,
|
||||||
|
ctx = ctx,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local open_formspecs = {}
|
||||||
|
function Form:show(player, ctx)
|
||||||
|
if type(player) == "string" then
|
||||||
|
player = minetest.get_player_by_name(player)
|
||||||
|
if not player then return end
|
||||||
|
end
|
||||||
|
|
||||||
|
local t = minetest.get_us_time()
|
||||||
|
ctx = ctx or {}
|
||||||
|
|
||||||
|
local name = player:get_player_name()
|
||||||
|
local info = minetest.get_player_information(name)
|
||||||
|
local tree, form_info = self:_render(player, ctx,
|
||||||
|
info and info.formspec_version)
|
||||||
|
|
||||||
|
local t2 = minetest.get_us_time()
|
||||||
|
local fs = assert(formspec_ast.unparse(tree))
|
||||||
|
local t3 = minetest.get_us_time()
|
||||||
|
|
||||||
|
open_formspecs[name] = form_info
|
||||||
|
print(t3 - t, t2 - t, t3 - t2)
|
||||||
|
minetest.show_formspec(name, self._formname, fs)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Form:show_hud(player, ctx)
|
||||||
|
local tree = self:_render(player, ctx or {})
|
||||||
|
hud_fs.show_hud(player, self._formname, tree)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Form:close(player)
|
||||||
|
minetest.close_formspec(player:get_player_name(), self._formname)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Form:close_hud(player)
|
||||||
|
hud_fs.close_hud(player, self._formname)
|
||||||
|
end
|
||||||
|
|
||||||
|
local used_ids = {}
|
||||||
|
setmetatable(used_ids, {__mode = "v"})
|
||||||
|
|
||||||
|
local formname_prefix = minetest and minetest.get_current_modname() or "" .. ":"
|
||||||
|
|
||||||
|
local form_mt = {__index = Form}
|
||||||
|
function flow.make_gui(build_func)
|
||||||
|
local res = setmetatable({}, form_mt)
|
||||||
|
|
||||||
|
-- Reserve a formname
|
||||||
|
local id = #used_ids + 1
|
||||||
|
used_ids[id] = gui
|
||||||
|
|
||||||
|
res._formname = formname_prefix .. get_identifier(id)
|
||||||
|
res._build = build_func
|
||||||
|
|
||||||
|
return res
|
||||||
|
end
|
||||||
|
|
||||||
|
local function on_fs_input(player, formname, fields)
|
||||||
|
local name = player:get_player_name()
|
||||||
|
local form_info = open_formspecs[name]
|
||||||
|
if not form_info then return end
|
||||||
|
|
||||||
|
if formname ~= form_info.formname then return end
|
||||||
|
|
||||||
|
local callbacks = form_info.callbacks
|
||||||
|
local ctx = form_info.ctx
|
||||||
|
local redraw_if_changed = form_info.redraw_if_changed
|
||||||
|
local ctx_form = ctx.form
|
||||||
|
|
||||||
|
-- Update the context before calling any callbacks
|
||||||
|
local redraw_fs = false
|
||||||
|
for field, transformer in pairs(form_info.saved_fields) do
|
||||||
|
if fields[field] then
|
||||||
|
local new_value = transformer(fields[field])
|
||||||
|
if redraw_if_changed[field] and ctx_form[field] ~= new_value then
|
||||||
|
print('Modified:', dump(field), dump(ctx_form[field]), '->',
|
||||||
|
dump(new_value))
|
||||||
|
redraw_fs = true
|
||||||
|
end
|
||||||
|
ctx_form[field] = new_value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Some callbacks may be false to indicate that they're valid fields but
|
||||||
|
-- don't need to be called
|
||||||
|
for field, value in pairs(fields) do
|
||||||
|
if callbacks[field] and callbacks[field](player, ctx, value) then
|
||||||
|
redraw_fs = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if open_formspecs[name] ~= form_info then return end
|
||||||
|
|
||||||
|
if fields.quit then
|
||||||
|
open_formspecs[name] = nil
|
||||||
|
elseif redraw_fs then
|
||||||
|
form_info.self:show(player, ctx)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function on_leaveplayer(player)
|
||||||
|
open_formspecs[player:get_player_name()] = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
if DEBUG_MODE then
|
||||||
|
flow.hot_reload = {on_fs_input, on_leaveplayer}
|
||||||
|
if not hot_reload[1] then
|
||||||
|
minetest.register_on_player_receive_fields(function(...)
|
||||||
|
return flow.hot_reload[1](...)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
if not hot_reload[2] then
|
||||||
|
minetest.register_on_leaveplayer(function(...)
|
||||||
|
return flow.hot_reload[2](...)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
minetest.register_on_player_receive_fields(on_fs_input)
|
||||||
|
minetest.register_on_leaveplayer(on_leaveplayer)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Extra GUI elements
|
||||||
|
|
||||||
|
-- Please don't use rawset(gui, ...) in your own code
|
||||||
|
rawset(gui, "PaginatedVBox", function(def)
|
||||||
|
local w, h = def.w, def.h
|
||||||
|
def.w, def.h = nil, nil
|
||||||
|
local paginator_name = "_paginator-" .. assert(def.name)
|
||||||
|
|
||||||
|
def.type = "vbox"
|
||||||
|
local inner_w, inner_h = get_and_fill_in_sizes(def)
|
||||||
|
h = h or min(inner_h, 5)
|
||||||
|
|
||||||
|
local ctx = flow.get_context()
|
||||||
|
|
||||||
|
-- Build a list of pages
|
||||||
|
local page = {}
|
||||||
|
local pages = {page}
|
||||||
|
local max_y = h
|
||||||
|
for _, node in ipairs(def) do
|
||||||
|
if node.y and node.y + (node.h or 0) > max_y then
|
||||||
|
-- Something overflowed, go to a new page
|
||||||
|
page = {}
|
||||||
|
pages[#pages + 1] = page
|
||||||
|
max_y = node.y + h
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Add to the current page
|
||||||
|
node.x, node.y = nil, nil
|
||||||
|
page[#page + 1] = node
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Get the current page
|
||||||
|
local current_page = ctx.form[paginator_name] or 1
|
||||||
|
if current_page > #pages then
|
||||||
|
current_page = #pages
|
||||||
|
ctx.form[paginator_name] = current_page
|
||||||
|
end
|
||||||
|
|
||||||
|
page = pages[current_page] or {}
|
||||||
|
page.h = h
|
||||||
|
|
||||||
|
return gui.VBox {
|
||||||
|
min_w = w or inner_w,
|
||||||
|
gui.VBox(page),
|
||||||
|
gui.HBox {
|
||||||
|
gui.Button {
|
||||||
|
label = "<",
|
||||||
|
on_event = function(_, ctx)
|
||||||
|
ctx.form[paginator_name] = max(current_page - 1, 1)
|
||||||
|
return true
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
gui.Label {
|
||||||
|
label = "Page " .. current_page .. " of " .. #pages,
|
||||||
|
align_h = "centre",
|
||||||
|
expand = true,
|
||||||
|
},
|
||||||
|
gui.Button {
|
||||||
|
label = ">",
|
||||||
|
on_event = function(_, ctx)
|
||||||
|
ctx.form[paginator_name] = current_page + 1
|
||||||
|
return true
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end)
|
||||||
|
|
||||||
|
rawset(gui, "ScrollableVBox", function(def)
|
||||||
|
-- On older clients fall back to a paginated vbox
|
||||||
|
if gui.formspec_version < 4 then
|
||||||
|
return gui.PaginatedVBox(def)
|
||||||
|
end
|
||||||
|
|
||||||
|
local w, h = def.w, def.h
|
||||||
|
local scrollbar_name = "_scrollbar-" .. assert(
|
||||||
|
def.name, "Please provide a name for all ScrollableVBox elements!"
|
||||||
|
)
|
||||||
|
|
||||||
|
def.type = "vbox"
|
||||||
|
def.x, def.y = 0, 0
|
||||||
|
def.w, def.h = nil, nil
|
||||||
|
local inner_w, inner_h = get_and_fill_in_sizes(def)
|
||||||
|
def.w = w or inner_w
|
||||||
|
def.expand = true
|
||||||
|
h = h or min(inner_h, 5)
|
||||||
|
|
||||||
|
return gui.HBox {
|
||||||
|
{
|
||||||
|
type = "scroll_container",
|
||||||
|
expand = true,
|
||||||
|
w = w or inner_w,
|
||||||
|
h = h,
|
||||||
|
scrollbar_name = scrollbar_name,
|
||||||
|
orientation = "vertical",
|
||||||
|
def,
|
||||||
|
},
|
||||||
|
gui.ScrollbarOptions{opts = {max = max(inner_h - h + 0.05, 0) * 10}},
|
||||||
|
gui.Scrollbar{
|
||||||
|
w = 0.5, h = 0.5,
|
||||||
|
orientation = "vertical",
|
||||||
|
name = scrollbar_name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end)
|
||||||
|
|
||||||
|
rawset(gui, "Flow", function(def)
|
||||||
|
local vbox = {
|
||||||
|
type = "vbox",
|
||||||
|
bgcolor = def.bgcolor,
|
||||||
|
bgimg = def.bgimg,
|
||||||
|
align_h = "centre",
|
||||||
|
align_v = "centre",
|
||||||
|
}
|
||||||
|
local width = assert(def.w)
|
||||||
|
|
||||||
|
local spacing = def.spacing or DEFAULT_SPACING
|
||||||
|
local line = {spacing = spacing}
|
||||||
|
for _, node in ipairs(def) do
|
||||||
|
local w = get_and_fill_in_sizes(node)
|
||||||
|
if w > width then
|
||||||
|
width = def.w
|
||||||
|
vbox[#vbox + 1] = gui.HBox(line)
|
||||||
|
line = {spacing = spacing}
|
||||||
|
end
|
||||||
|
line[#line + 1] = node
|
||||||
|
width = width - w - spacing
|
||||||
|
end
|
||||||
|
vbox[#vbox + 1] = gui.HBox(line)
|
||||||
|
return vbox
|
||||||
|
end)
|
||||||
|
|
||||||
|
local modpath = minetest.get_modpath("flow")
|
||||||
|
local example_form
|
||||||
|
minetest.register_chatcommand("flow-example", {
|
||||||
|
privs = {server = true},
|
||||||
|
help = "Shows an example formspec",
|
||||||
|
func = function(name)
|
||||||
|
-- Only load example.lua when it's needed
|
||||||
|
if not example_form then
|
||||||
|
example_form = dofile(modpath .. "/example.lua")
|
||||||
|
end
|
||||||
|
example_form:show(name)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
|
||||||
|
if DEBUG_MODE then
|
||||||
|
local f, err = loadfile(modpath .. "/test-fs.lua")
|
||||||
|
if not f then
|
||||||
|
minetest.log("error", "[flow] " .. tostring(err))
|
||||||
|
end
|
||||||
|
return f()
|
||||||
|
end
|
Loading…
Reference in New Issue
Block a user