[Mod] flow (formspec library and layout manager) [flow]

Post Reply
User avatar
luk3yx
Member
Posts: 83
Joined: Sun Oct 21, 2012 18:14
GitHub: luk3yx
IRC: luk3yx
In-game: luk3yx
Location: Earth
Contact:

[Mod] flow (formspec library and layout manager) [flow]

by luk3yx » Post

flow

Image

An experimental layout manager and formspec API replacement for Minetest. Vaguely inspired by Flutter and GTK.

Online tutorial/demo (source)

Features

Layouting
  • No manual positioning of elements.
  • Automatic layouting using HBox and VBox containers
  • Some elements have an automatic size.
  • The size of elements can optionally expand to fit larger spaces
Other features
  • 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 form and are automatically applied.
Limitations
  • This mod doesn't support all of the features that regular formspecs do.
  • 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.

Code: Select all

-- 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.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)
Installing

You can install flow from ContentDB or by cloning the GitHub or GitLab repositories.

License

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/.

Other formspec libraries/utilities

These utilities likely aren't compatible with flow.
  • fs_layout is another mod library that does automatic formspec element positioning.
  • fslib is a small mod library that lets you build formspec strings.
  • Just_Visiting's formspec editor is a Minetest (sub)game that lets you edit formspecs and preview them as you go
  • 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 lets you add elements and drag+drop them, however it doesn't support all formspec features.
Elements

See the README file on GitHub or GitLab for more documentation.
Last edited by luk3yx on Sat Dec 03, 2022 06:30, edited 1 time in total.
git_undo() { [ -e .git ] || return 1; local r=$(git remote get-url origin); cd ..; rm -rf "$OLDPWD"; git clone "$r" "$OLDPWD"; cd "$OLDPWD"; }

doxygen_spammer
Member
Posts: 70
Joined: Wed Dec 16, 2020 16:52
GitHub: doxygen-spammer

Re: [Mod] flow (formspec library and layout manager) [flow]

by doxygen_spammer » Post

luk3yx wrote:
Fri Jul 15, 2022 00:49
  • No manual positioning of elements.
Hey, this is exactly what I am looking for right now.
(I need a form for a machine like a furnace, where the number of inventory slots can vary.)
Vaguely inspired by Flutter and GTK.
And that makes it sound very interesting!

I am also a bit disappointed that there is nothing comparable on ContentDB.
There are various formspec libraries, but they are all just string builders, without layouting functionality.
There are also layouting libraries, but not available on ContentDB.
Therefore I will very likely use your library. :)

I just suggest that you rework the initial section of the introduction.
There are features about layouting and showing to players.
You should point out the layouting features much better. :)

Like this:

Features

  • Automatic GUI layout!
    • Based on layout containers, like seen in Flutter/GTK.
    • No manual positioning of elements.
    • Some elements have an automatic size.
    • The size of elements can optionally expand to fit larger spaces
  • Wrapper for formspec API
    • 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.
Besides that, I am happy that you have written extensive and clear documentation.

I feel more comfortable if I can use a library component wise.
In this case, I would like to get a formspec string, whether just for debugging purposes, or to manually show it to a player using original Minetest API.
After all, formspec strings are still the single guarranteed baseline all over Minetest. :/
I see that formspec_ast offers the unparse() function, so I think I will feel comfortable with your library.

User avatar
luk3yx
Member
Posts: 83
Joined: Sun Oct 21, 2012 18:14
GitHub: luk3yx
IRC: luk3yx
In-game: luk3yx
Location: Earth
Contact:

Re: [Mod] flow (formspec library and layout manager) [flow]

by luk3yx » Post

doxygen_spammer wrote:
Tue Nov 29, 2022 21:16
I just suggest that you rework the initial section of the introduction.
There are features about layouting and showing to players.
You should point out the layouting features much better. :)
I've added "layouting" and "other features" headings to the features list.
doxygen_spammer wrote:
Tue Nov 29, 2022 21:16
I feel more comfortable if I can use a library component wise.
In this case, I would like to get a formspec string, whether just for debugging purposes, or to manually show it to a player using original Minetest API.
After all, formspec strings are still the single guarranteed baseline all over Minetest. :/
I see that formspec_ast offers the unparse() function, so I think I will feel comfortable with your library.
Currently there isn't an API for this, formspec_ast.unparse only converts AST into a formspec string and doesn't do any layouting. The ScrollableVBox and PaginatedVBox elements rely on state tracking to work properly and wouldn't work in a function that returns a formspec string.
git_undo() { [ -e .git ] || return 1; local r=$(git remote get-url origin); cd ..; rm -rf "$OLDPWD"; git clone "$r" "$OLDPWD"; cd "$OLDPWD"; }

doxygen_spammer
Member
Posts: 70
Joined: Wed Dec 16, 2020 16:52
GitHub: doxygen-spammer

Re: [Mod] flow (formspec library and layout manager) [flow]

by doxygen_spammer » Post

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.)
Attachments
Screenshot_20221204_154254.png
Screenshot_20221204_154254.png (32.6 KiB) Viewed 1182 times

doxygen_spammer
Member
Posts: 70
Joined: Wed Dec 16, 2020 16:52
GitHub: doxygen-spammer

Re: [Mod] flow (formspec library and layout manager) [flow]

by doxygen_spammer » Post

luk3yx wrote:
Sat Dec 03, 2022 06:29
doxygen_spammer wrote:
Tue Nov 29, 2022 21:16
I would like to get a formspec string
Currently there isn't an API for this, formspec_ast.unparse only converts AST into a formspec string and doesn't do any layouting. The ScrollableVBox and PaginatedVBox elements rely on state tracking to work properly and wouldn't work in a function that returns a formspec string.
Indeed I didn’t feel the need to get a formspec string.
I just wanted to try anyways, and it is actually pretty easy.

Code: Select all

local player = {
    get_player_name = function() return "hey"; end;
};

minetest.debug(formspec_ast.unparse(form:new():make_session():_render(player, {}, 4)));
Gives the result: formspec_version[5]size[10.35,11.1]background9[0,0;0,0;doxy_plush_ult...

get_player_name() is called by my own build function only.
Of couse it would be more difficult with PaginatedVBox etc. :)

But I still think this may be useful sometimes.
For example, the automatically updating machine GUI would not be possible with the current functionality of make_gui() and Form:show(), but would be possible if I call minetest.show_formspec() myself.

User avatar
luk3yx
Member
Posts: 83
Joined: Sun Oct 21, 2012 18:14
GitHub: luk3yx
IRC: luk3yx
In-game: luk3yx
Location: Earth
Contact:

Re: [Mod] flow (formspec library and layout manager) [flow]

by luk3yx » Post

doxygen_spammer wrote:
Sun Dec 04, 2022 16:22
Having the layout in a closure is not too bad.
It just increases the indenting of everything by one or two tab widthes.
I used closures for the layout function so you can initialise ctx values and so that flow can reshow the form when necessary. I intended for flow.make_gui to usually be called during mod load time (though calling it at runtime is supported).
doxygen_spammer wrote:
Sun Dec 04, 2022 16:22
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.
I think the best way to do this would be to do on_event = some_local_function and store any values you need to access from the callback in ctx. I think closures are okay for simple callbacks, but if you do a lot more then using a separate function is probably better.
doxygen_spammer wrote:
Sun Dec 04, 2022 16:22
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.
The form:show function accepts a ctx parameter (form:show(player, custom_ctx)), maybe I should document this somewhere. ctx.form will be added to custom_ctx if it doesn't already exist.

I didn't think of making forms classes when I designed the API.
doxygen_spammer wrote:
Sun Dec 04, 2022 16:22
(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>().)
I'm not sure about allowing the internal Form class to be subclassed, I'd probably have to create an OOP API and worry about breaking mods if I add a new function to the class.
doxygen_spammer wrote:
Sun Dec 04, 2022 16:22
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.
The builder function can return any GUI element so you could probably just call the return value an element, though maybe element tree.

I think the object returned by flow.make_gui should be called a form or GUI, the tutorial uses calls the variable my_gui.
doxygen_spammer wrote:
Sun Dec 04, 2022 16:22

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
That's an interesting idea. It's possible to replicate the animated image code without using display:

Code: Select all

self:is_running() and gui.AnimatedImage{
    w = 1;
    h = 0.5;
    texture_name = "doxy_plush_ultrasonic_cleaner_brrrr.png";
    frame_count = 4;
    frame_duration = 125;
} or gui.Image{
    w = 1;
    h = 0.5;
    texture_name = "doxy_plush_ultrasonic_cleaner_brr.png";
};
Though I'd understand if you think that looks ugly.
doxygen_spammer wrote:
Sun Dec 04, 2022 16:22
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.
You could probably do something like the above and/or code for now, though I think a simple implementation would just be this:

Code: Select all

local function Stacked(def)
    return def[def.selected]
end

Stacked{
    selected = 1,
    gui.Label{label = "Element 1"},
    gui.Label{label = "Element 2"},
    gui.Label{label = "Element 3"},
}
However this wouldn't work if the elements were different sizes. I'm also not sure how useful it would be for anything with more than two elements. If it somehow allowed multiple elements to be visible at once it could probably be used to create fancy buttons that contain Image and Label elements.
doxygen_spammer wrote:
Sun Dec 04, 2022 16:22
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.
I think that's a good idea, though maybe it should be called cell_bgimg or something if it would be added to individual slots in larger lists.
doxygen_spammer wrote:
Sun Dec 04, 2022 16:22
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.)
I've thought about adding a form:update function. I'm not sure how you'd only update the form for certain players (for example ones with a specific machine open) without using one form object per machine.

The metatable idea sounds complicated and I don't think that any modification to nested tables (external_data.pos.y = external_data.pos.y + 1) would be detected without dynamically creating fake tables as needed.
doxygen_spammer wrote:
Sun Dec 04, 2022 16:22
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.)
That sounds easier to use, though implementing it in formspec_ast probably wouldn't be very easy.
doxygen_spammer wrote:
Sun Dec 04, 2022 16:22
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.
I was thinking of just adding a CSS-like language to flow but I decided it would be too complicated.
doxygen_spammer wrote:
Sun Dec 04, 2022 16:22
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 suggest just making functions for custom GUI widgets:

Code: Select all

-- Example MachineGui function
local function MachineGui(def)
    def.bgimg, def.bgimg_middle = get_background()
    table.insert(def, 1, gui.Label{label="Machine"})
    def[#def + 1] = gui.List{
        inventory_location = "current_player",
        listname = "main",
        w = 8,
        h = 4,
    }
    return gui.VBox(def)
end
doxygen_spammer wrote:
Sun Dec 04, 2022 16:22
I see you already announced a tutorial called “Expansion”, which I am looking forward for. :)
The "expansion" tutorial is going to cover the expand = true field if I ever get around to writing it, and not expanding the API/adding new elements. Maybe the tutorial needs a better name.
doxygen_spammer wrote:
Sun Dec 04, 2022 16:22
Do you welcome merge requests?
If I like the idea and implementation.

Thank you for your feedback! Hopefully I haven't missed anything in my reply.
git_undo() { [ -e .git ] || return 1; local r=$(git remote get-url origin); cd ..; rm -rf "$OLDPWD"; git clone "$r" "$OLDPWD"; cd "$OLDPWD"; }

doxygen_spammer
Member
Posts: 70
Joined: Wed Dec 16, 2020 16:52
GitHub: doxygen-spammer

Re: [Mod] flow (formspec library and layout manager) [flow]

by doxygen_spammer » Post

luk3yx wrote:
Mon Dec 05, 2022 06:20
doxygen_spammer wrote:
Sun Dec 04, 2022 16:22
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.
The form:show function accepts a ctx parameter (form:show(player, custom_ctx)), maybe I should document this somewhere. ctx.form will be added to custom_ctx if it doesn't already exist.
Indeed, that is good to know.
I didn't think of making forms classes when I designed the API.
(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>().)
I'm not sure about allowing the internal Form class to be subclassed, I'd probably have to create an OOP API and worry about breaking mods if I add a new function to the class.
You probably mean a user could implement my_form:_is_active(machine_pos), and later you want to implement Form:_is_active() with a different meaning, or similar?
That sounds reasonable.

Maybe the Form object could be a member of some other class, which provides the interface to users.
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.)
I've thought about adding a form:update function. I'm not sure how you'd only update the form for certain players (for example ones with a specific machine open) without using one form object per machine.

The metatable idea sounds complicated and I don't think that any modification to nested tables (external_data.pos.y = external_data.pos.y + 1) would be detected without dynamically creating fake tables as needed.
You are right, I didn’t think about nested tables.

My next idea is that you can describe with a list of arbitrary strings on which external states a form depends.
Then you later notify which of these states have changed, but do not provide the new state.
Like this:

Code: Select all

local my_machine_gui = flow.make_gui(my_form_builder);
...
function machine_node_definition.on_rightclick(pos, player)
    local ctx = {
        pos = pos;
        -- The dependency list should be part of `ctx`,
        -- so the form builder function also has a chance to update it.
        dependencies = { "machine_" .. pos:to_string() };
    };

    my_machine_gui:show(player, ctx);
end

function machine_node_definition.on_timer(pos)
    ...
    my_machine_gui:update("machine_" .. pos:to_string());
end
I see you already announced a tutorial called “Expansion”, which I am looking forward for. :)
The "expansion" tutorial is going to cover the expand = true field if I ever get around to writing it, and not expanding the API/adding new elements. Maybe the tutorial needs a better name.
Oops :D
Do you welcome merge requests?
If I like the idea and implementation.
I would like to think about a second layer of API, that allows to create form classes.
Probably flow.make_gui() could return a FlowGui object (so it has a clear name ;) ), and FlowGui can also be manually subclassed to override a build function and implement callback handler methods.

User avatar
luk3yx
Member
Posts: 83
Joined: Sun Oct 21, 2012 18:14
GitHub: luk3yx
IRC: luk3yx
In-game: luk3yx
Location: Earth
Contact:

Re: [Mod] flow (formspec library and layout manager) [flow]

by luk3yx » Post

doxygen_spammer wrote:
Mon Dec 05, 2022 11:18
You probably mean a user could implement my_form:_is_active(machine_pos), and later you want to implement Form:_is_active() with a different meaning, or similar?
That sounds reasonable.
Yes.
doxygen_spammer wrote:
Mon Dec 05, 2022 11:18
My next idea is that you can describe with a list of arbitrary strings on which external states a form depends.
Then you later notify which of these states have changed, but do not provide the new state.
Like this:

Code: Select all

local my_machine_gui = flow.make_gui(my_form_builder);
...
function machine_node_definition.on_rightclick(pos, player)
    local ctx = {
        pos = pos;
        -- The dependency list should be part of `ctx`,
        -- so the form builder function also has a chance to update it.
        dependencies = { "machine_" .. pos:to_string() };
    };

    my_machine_gui:show(player, ctx);
end

function machine_node_definition.on_timer(pos)
    ...
    my_machine_gui:update("machine_" .. pos:to_string());
end
I don't like relying on the string representation of pos being the same, though that probably isn't a big issue since I don't see why it'd be different.

Some more ideas:

Code: Select all

-- Update for every player where the condition function matches
-- Something like this would also allow updating a form for all players with a certain privilege for example
my_gui:update_where(function(player, ctx)
    return vector.equals(ctx.machine_pos, pos)
end)
Or maybe an iterator? Though I think I'd prefer the closure or dependencies over this

Code: Select all

for player, ctx in my_gui:open_forms() do
    if vector.equals(ctx.machine_pos, pos) then
        my_gui:update(player)
    end
end
Or (in MT 5.5.0+):

Code: Select all

my_gui:show(player, {machine_pos = pos})

-- Updates all forms where ctx.machine_pos == pos
my_gui:update_where({machine_pos = pos})
doxygen_spammer wrote:
Mon Dec 05, 2022 11:18
I would like to think about a second layer of API, that allows to create form classes.
Probably flow.make_gui() could return a FlowGui object (so it has a clear name ;) ), and FlowGui can also be manually subclassed to override a build function and implement callback handler methods.
Something like this?

Code: Select all

-- This code won't work with flow, I'm just using it to demonstrate what the API would look like
local MyForm = setmetatable({}, {__index = flow.Form})

function MyForm:build(player, ctx)
    return gui.VBox{
        gui.Label{label = "Button presses: " .. (ctx.count or 0)},
        gui.Button{name = "my_btn", label = "Press me!"},
    }
end

function MyForm:on_event_my_btn(player, ctx)
    ctx.count = (ctx.count or 0) + 1
    minetest.chat_send_player(player:get_player_name(), "Button pressed!")
    return true
end

local form = MyForm:new()

minetest.register_chatcommand("my-form", {
    func = function(name)
        form:show(minetest.get_player_by_name(name))
    end
})
I'm not sure about making subclasses that will only have one instance
git_undo() { [ -e .git ] || return 1; local r=$(git remote get-url origin); cd ..; rm -rf "$OLDPWD"; git clone "$r" "$OLDPWD"; cd "$OLDPWD"; }

doxygen_spammer
Member
Posts: 70
Joined: Wed Dec 16, 2020 16:52
GitHub: doxygen-spammer

Re: [Mod] flow (formspec library and layout manager) [flow]

by doxygen_spammer » Post

luk3yx wrote:
Fri Dec 09, 2022 04:49
doxygen_spammer wrote:
Mon Dec 05, 2022 11:18
My next idea is that you can describe with a list of arbitrary strings on which external states a form depends.
Then you later notify which of these states have changed, but do not provide the new state.
[...]

Some more ideas:

Code: Select all

-- Update for every player where the condition function matches
-- Something like this would also allow updating a form for all players with a certain privilege for example
my_gui:update_where(function(player, ctx)
    return vector.equals(ctx.machine_pos, pos)
end)
Yeah, a closure here is a good idea.
It moves the point where “dependencies” of a form are defined to the same point where they are modified.
Slightly less spaghetti. :)
I would like to think about a second layer of API, that allows to create form classes.
Probably flow.make_gui() could return a FlowGui object (so it has a clear name ;) ), and FlowGui can also be manually subclassed to override a build function and implement callback handler methods.
Something like this?

[...]

I'm not sure about making subclasses that will only have one instance
Yes, similar to that.

I would rather make the show() method create a new instance of the class (an “object”), which is used as ctx for Form:show().
(So the instances of the subclass are actually used to hold data.)

I write some creativity dump here.
wire_up_callback_handlers() makes the code theoretically actually work with the current version of flow.

Code: Select all

flow.FormGui = {}

function flow.FormGui:new(player)
	return setmetatable({}, { __index = self })
end

function flow.FormGui:build(player)
	return gui.Label{ label = "You are ‘" .. player:get_player_name() .. "’. Please reimplement FormGui:build() in your GUI class." }
end

-- This function adds wrappers of callback handler methods to the widget tree.
-- Alternatively, on_fs_input() would check if ctx has a method for that field name.
local function wire_up_callback_handlers(widget, gui_class)
	if widget.name and gui_class["on_event_" .. widget.name] then
		widget.on_event = function(player, ctx)
			ctx["on_event_" .. widget.name](ctx, player)
		end
	end

	for _, subwidget in ipairs(widget) do -- This iterator is probably too silly.
		wire_up_callback_handlers(subwidget, gui_class)
	end
end

local gui_per_class = {}
function flow.FormGui:show(player, init_data)
	if not gui_per_class[self] then
		-- Here it creates a build function, happening the first time when this subclass of FormGui is used.
		gui_per_class[self] = flow.make_gui(function(player2, ctx2)
			local widget = ctx2:build(player2)
			wire_up_callback_handlers(widget, ctx2)
			return widget
		end)
	end

	-- An instance of the GUI class is used as context for the form session of `player`.
	local ctx = self:new()
	if self.init then
		ctx:init(player, init_data) -- Bonus ;)
	end
 	gui_per_class[self]:show(player, ctx)
 end

Code: Select all

local my_form = flow.FormGui:new();

function my_form:init(player)
	minetest.chat_send_player(player:get_player_name(), "Welcome to my_form!");
end

function my_form:build(player)
	return gui.VBox{
		gui.Checkbox{
			name = "check";
			label = "I am " .. (self.form.check and "checked" or "not checked") .. " :D";
		};
		gui.Button{
			name = "button";
			label = "Hello!";
		};
	};	
end

function my_form:on_event_button(player)
	minetest.chat_send_player(player:get_player_name(), "Hi!");

	-- FormGui:show() makes an instance of this class.
	assert(self ~= my_form);
	assert(getmetatable(self).__index == my_form);
end

minetest.register_chatcommand("my-form", {
	func = function(playername)
		my_form:show(minetest.get_player_by_name(playername));
	end
})

User avatar
luk3yx
Member
Posts: 83
Joined: Sun Oct 21, 2012 18:14
GitHub: luk3yx
IRC: luk3yx
In-game: luk3yx
Location: Earth
Contact:

Re: [Mod] flow (formspec library and layout manager) [flow]

by luk3yx » Post

Hi, sorry about the late reply.
doxygen_spammer wrote:
Fri Dec 09, 2022 12:22
Yeah, a closure here is a good idea.
It moves the point where “dependencies” of a form are defined to the same point where they are modified.
Slightly less spaghetti. :)
I've implemented form:update() and form:update_where(). Note that these methods won't work properly if another formspec is shown over top of them. There's also a race condition if the player presses escape before the new "show formspec" packet reaches the client, it'd be possible for flow to work around this by doing close_formspec the player closes a form but the formspec would appear and then disappear again on the client.

I've added minetest.after to the "flow playground" if you wanted to try the new function.
doxygen_spammer wrote:
Fri Dec 09, 2022 12:22
I would rather make the show() method create a new instance of the class (an “object”), which is used as ctx for Form:show().
(So the instances of the subclass are actually used to hold data.)

I write some creativity dump here.
wire_up_callback_handlers() makes the code theoretically actually work with the current version of flow.

Code: Select all

flow.FormGui = {}

function flow.FormGui:new(player)
	return setmetatable({}, { __index = self })
end

function flow.FormGui:build(player)
	return gui.Label{ label = "You are ‘" .. player:get_player_name() .. "’. Please reimplement FormGui:build() in your GUI class." }
end

-- This function adds wrappers of callback handler methods to the widget tree.
-- Alternatively, on_fs_input() would check if ctx has a method for that field name.
local function wire_up_callback_handlers(widget, gui_class)
	if widget.name and gui_class["on_event_" .. widget.name] then
		widget.on_event = function(player, ctx)
			ctx["on_event_" .. widget.name](ctx, player)
		end
	end

	for _, subwidget in ipairs(widget) do -- This iterator is probably too silly.
		wire_up_callback_handlers(subwidget, gui_class)
	end
end

local gui_per_class = {}
function flow.FormGui:show(player, init_data)
	if not gui_per_class[self] then
		-- Here it creates a build function, happening the first time when this subclass of FormGui is used.
		gui_per_class[self] = flow.make_gui(function(player2, ctx2)
			local widget = ctx2:build(player2)
			wire_up_callback_handlers(widget, ctx2)
			return widget
		end)
	end

	-- An instance of the GUI class is used as context for the form session of `player`.
	local ctx = self:new()
	if self.init then
		ctx:init(player, init_data) -- Bonus ;)
	end
 	gui_per_class[self]:show(player, ctx)
 end

Code: Select all

local my_form = flow.FormGui:new();

function my_form:init(player)
	minetest.chat_send_player(player:get_player_name(), "Welcome to my_form!");
end

function my_form:build(player)
	return gui.VBox{
		gui.Checkbox{
			name = "check";
			label = "I am " .. (self.form.check and "checked" or "not checked") .. " :D";
		};
		gui.Button{
			name = "button";
			label = "Hello!";
		};
	};	
end

function my_form:on_event_button(player)
	minetest.chat_send_player(player:get_player_name(), "Hi!");

	-- FormGui:show() makes an instance of this class.
	assert(self ~= my_form);
	assert(getmetatable(self).__index == my_form);
end

minetest.register_chatcommand("my-form", {
	func = function(playername)
		my_form:show(minetest.get_player_by_name(playername));
	end
})
Your code works with the current version of flow.
This iterator is probably too silly.
I think it's the best way of iterating over all of the elements.
git_undo() { [ -e .git ] || return 1; local r=$(git remote get-url origin); cd ..; rm -rf "$OLDPWD"; git clone "$r" "$OLDPWD"; cd "$OLDPWD"; }

Post Reply

Who is online

Users browsing this forum: No registered users and 5 guests