I can happily show my result of working with
flow.
I used the following “build function”:
Spoiler
Code: Select all
--! Creates a Flow based form for the ultrasonic cleaner.
--!
--! @param player is the player using this form.
--! @param context holds information about the form session.
--!
--! @returns a tree created by @c flow.widgets, let me call this a flow layout.
function ultrasonic_cleaner_form:make_layout(player, context)
return gui.VBox{
bgimg = select(1, self:bg_image());
bgimg_middle = select(2, self:bg_image());
gui.Label{
label = S("Ultrasonic Cleaner");
};
gui.HBox{
align_h = "centre";
optional(gui.VBox){
gui.Spacer{};
gui.HBox{
optional(gui.VBox){
gui.List{
inventory_location = self:inventory_location();
list_name = "src";
w = 1;
h = 1;
};
gui.Image{
w = 1;
h = 0.5;
display = not self:is_running();
texture_name = "doxy_plush_ultrasonic_cleaner_brr.png";
};
gui.AnimatedImage{
w = 1;
h = 0.5;
display = self:is_running();
texture_name = "doxy_plush_ultrasonic_cleaner_brrrr.png";
frame_count = 4;
frame_duration = 125;
};
gui.HBox{
gui.VBox{
bgimg = "doxy_plush_ultrasonic_cleaner_bucket.png";
gui.List{
inventory_location = self:inventory_location();
list_name = "water";
w = 1;
h = 1;
};
};
gui.VBox{
bgimg = "doxy_plush_ultrasonic_cleaner_solvent.png";
gui.List{
inventory_location = self:inventory_location();
list_name = "solvent";
w = 1;
h = 1;
};
};
};
};
gui.Image{
align_v = "centre";
w = 1;
h = 1;
texture_name = "doxy_plush_ultrasonic_cleaner_arrow.png";
};
};
gui.Spacer{};
gui.Checkbox{
name = "public_checkbox";
label = S("Public");
selected = self:is_public();
display = self:is_owned(player);
on_event = function(player2, context2)
self:on_public_toggle(player2, context2);
end;
};
};
gui.List{
align_v = "centre";
inventory_location = self:inventory_location();
list_name = "dst";
w = 4;
h = 4;
};
};
gui.Spacer{};
gui.List{
inventory_location = "current_player";
list_name = "main";
w = 8;
h = 4;
};
};
end
I can confirm that creating a layout is easy with this library, and that I can finally stop thinking in coordinates. :)
The only required numbers are sizes of images and inventory lists.
API
But the API feels very clumsy.
It assumes that I want to generate the whole layout within one closure, and that callback handlers shall be closures too.
I agree that upvalues and closures in Lua are pretty cool, but they bring problems in practical life.
Closures
Having the layout in a closure is not too bad.
It just increases the indenting of everything by one or two tab widthes.
Having callback handlers in closures is bad.
In many layouts, the
on_event item is already deeply indented.
To write useful code there, I either need a bigger screen or call another function from the closure.
Callback parameters
I decided to write a class for this form, which can hold the callback handlers.
But I faced another problem with the API: I am allowed to modify the
ctx table, but not to provide it.
Therefore I need tiny closures everywhere to get around
ctx and access the class.
If I was allowed to initialize
ctx with my class object, and the parameters of all callbacks were swapped (so
self comes first), I could simply name the callbacks with
self.callback_name.
(Actually, if Flow would provide a form class to be subclassed, I wouldn’t even need to specify callback handlers.
I would just implement methods, which are named in a pattern like on_event_<element>().)
Documentation
While writing the class, I felt quite silly to describe return values as “flow”.
It is a very undescriptive word to describe return values.
I made up the terms “flow layout” and “flow session” for the tables returned by the build function and
flow.make_gui().
It would be nice if you could add documentation that gives explicit names to these data types.
That would also help to write concise and exact documentation for the various element constructors.
Features
Display and visibility
I like that I can describe a “layout” by nesting a bunch of table constructors.
That results in a readable section of quite passive code, representing the layout hierarchy directly.
Much like HTML, and in the spirit of “Lua as configuration language”.
But often, elements appear and disappear in the layout, based on external states.
It would be useful to have
display and
visible properties for all layout elements, with the same meaning as in CSS.
display would allow me to remove a layout subtree, so other elements can use the space; while
visible would just remove the formspec elements, so the space of these elements remains empty.
(CSS would allow to show subelements of invisible elements, when they are explicitly visible.)
I implemented
display for my purposes:
Code: Select all
--! Removes elements with @c display being false from a layout element list,
--! and creates a @p widget from the remaining elements.
local function optional(widget)
return function(t)
for i = #t, 1, -1 do
if t[i].display == false then
table.remove(t, i);
end
end
return widget(t);
end
end
Related to this, it would be handy to have a “stacked widget”, where all subelements are layouted in the same place, but only the one selected by an index is visible.
Slot background images
I have two inventory slots with a background image.
Adding the background image is already quite easy with Flow, I just need to wrap the slot in a box with
bgimg given.
It would be even cooler if the
List element supports
bgimg itself.
Updating forms
It is currently not possible to update a form shown to a player, because Flow does not tell me whether the form is still shown or already closed.
Therefore I can not switch between “on” and “off” versions of a machine GUI automatically, but only when the player opens a new form.
I would like to either get an
on_exit callback, a
update method, or to make Flow handle updates for me.
Of course I would prefer the last option. :D
That would require the form to be created by a factory class, like the one I have made for myself.
The class itself would hold a table for external states, and I am allowed to update elements in this table.
Flow would detect updates (just like the metatable on
ctx.form does), and reshow all form instances that are currently open.
(The external state tables need to be prefixed in some flexible way, so I can report state changes of individual machine nodes.)
Inventory locations
It would be handy to specify the “location” of a node inventory directly with a location vector, instead of with a string.
I am working around it like this:
Code: Select all
--! Returns a string for the inventory location in formspec format.
function ultrasonic_cleaner_form:inventory_location()
return string.format("nodemeta:%i,%i,%i", self.pos.x, self.pos.y, self.pos.z);
end
(This feature request probably belongs to formspec_ast.)
Styles
Minetest’s style elements are inconvenient.
It would be cool if I could specify
props not just on a
Style element, but directly on widgets that shall be affected.
Subwidgets should probably be affected too, like in CSS.
Templates
I can imagine there are several forms that share a few common elements.
In my case, the form for the ultrasonic cleaner shares the heading line and the player inventory with all other machine GUIs.
An element called “MachineGui” would be appropriate.
I see you already announced a tutorial called “Expansion”, which I am looking forward for. :)
Do you welcome merge requests?
BTW here is the remaining part of my class:
Spoiler
Code: Select all
local S = minetest.get_translator("doxy_plush");
local V = vector.new;
local gui = flow.widgets;
--! @class ultrasonic_cleaner_form
--! Factory class for Flow dialog sessions for the ultrasonic cleaner GUI.
local ultrasonic_cleaner_form = {
--! Position of the machine.
pos = V(0, 0, 0);
};
--! Initializes a session factory for the machine at @p pos.
function ultrasonic_cleaner_form:new(pos)
return setmetatable({ pos = pos }, { __index = self });
end
--! @pure
--! Whether the public toggle checkbox shall be checked.
function ultrasonic_cleaner_form:is_public()
return false;
end
--! @pure
--! Whether the public toggle checkbox shall be visible to @p player.
function ultrasonic_cleaner_form:is_owned(player)
local _unused = player;
return false;
end
--! @pure
--! Callback handler for the public toggle checkbox.
function ultrasonic_cleaner_form:on_public_toggle(player, context)
local _unused1, _unused2 = player, context;
return false;
end
--! @pure
--! Whether the machine is running.
function ultrasonic_cleaner_form:is_running()
return false;
end
--! @pure
--! Background image for the dialog.
function ultrasonic_cleaner_form:bg_image()
return "()", "0";
end
--! Creates a dialog to be used for one session.
--!
--! @returns a table with show(player) method, let me call this a flow session.
function ultrasonic_cleaner_form:make_session()
return flow.make_gui(
function(player, context)
return self:make_layout(player, context);
end
);
end
return ultrasonic_cleaner_form;
(The “pure” methods are implemented in the file where this class is used, so I have GUI and logic well separated.)