Initial commit
commit
4c7996c915
|
@ -0,0 +1,2 @@
|
|||
test*.lua
|
||||
*.old
|
|
@ -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"}
|
|
@ -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.
|
|
@ -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.
|
|
@ -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!",
|
||||
}
|
||||
```
|
|
@ -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
|
|
@ -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)
|
|
@ -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