feat: Add render_to_formspec_string function (#5)

Co-authored-by: luk3yx <luk3yx@users.noreply.github.com>
This commit is contained in:
Lazerbeak12345 2023-04-07 19:13:44 -06:00 committed by GitHub
parent fa13e188f9
commit 60d2674a45
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 161 additions and 17 deletions

View File

@ -364,19 +364,20 @@ gui.Button{
},
```
## Experimental features
The style elements are invisible and won't affect padding.
These features might be broken in the future.
### Hiding elements
## Hiding elements
Elements inside boxes can have `visible = false` set to hide them from the
player. Elements hidden this way will still take up space like with
`visibility: hidden;` in CSS.
The style elements are invisible and won't affect padding.
## Experimental features
### `no_prepend[]`
These features might be broken in the future.
<details>
<summary><b><code>no_prepend[]</code></b></summary>
You can set `no_prepend = true` on the "root" element to disable formspec
prepends.
@ -400,7 +401,8 @@ end)
![Screenshot](https://user-images.githubusercontent.com/3182651/212222545-baee3669-15cd-410d-a638-c63b65a8811b.png)
### Using a form as an inventory
</details><details>
<summary><b>Using a form as an inventory</b></summary>
A form can be set as the player inventory. Flow internally generates the
formspec and passes it to `player:set_inventory_formspec()`. This will
@ -434,3 +436,28 @@ example_inventory:unset_as_inventory_for(player)
This will set the inventory formspec string to `""` and stop flow from
processing inventory formspec input.
</details><details>
<summary><b>Rendering to a formspec</b></summary>
This API should only be used when necessary and may have breaking changes in
the future.
Some APIs in other mods, such as sfinv, expect formspec strings. You can use
this API to embed flow forms inside them. To use flow with these mods, you can
call `form:render_to_formspec_string(player, ctx, standalone)`.
- By default the the `formspec_version` and `size` elements aren't included in
the returned formspec and are included in a third return value. Set
`standalone` to include them in the returned formspec string. The third
return value will not be returned.
- Returns `formspec, process_event[, info]`
- The `process_event(fields)` callback will return true if the formspec should
be redrawn, where `render_to_formspec_string` should be called and the new
`process_event` should be used in the future. This function may return true
even if fields.quit is sent.
**Do not use this API with node meta formspecs, it can and will break!**
</details>

View File

@ -842,6 +842,47 @@ function Form:set_as_inventory_for(player, ctx)
player:set_inventory_formspec(fs)
end
-- Declared here to be accessible by render_to_formspec_string
local fs_process_events
-- Prevent collisions in forms, but also ensure they don't happen across
-- mutliple embedded forms within a single parent.
-- Unique per-user to prevent players from making the counter wrap around for
-- other players.
local render_to_formspec_auto_name_ids = {}
-- If `standalone` is set, this will return a standalone formspec, otherwise it
-- will return a formspec that can be embedded and a table with its size and
-- target formspec version
function Form:render_to_formspec_string(player, ctx, standalone)
local name = player:get_player_name()
local info = minetest.get_player_information(name)
local tree, form_info = self:_render(player, ctx or {},
info and info.formspec_version, render_to_formspec_auto_name_ids[name])
local public_form_info
if not standalone then
local size = table.remove(tree, 1)
public_form_info = {w = size.w, h = size.h,
formspec_version = tree.formspec_version}
tree.formspec_version = nil
end
local fs = assert(formspec_ast.unparse(tree))
render_to_formspec_auto_name_ids[name] = form_info.auto_name_id
local function event(fields)
-- Just in case the player goes offline, we should not keep the player
-- reference. Nothing prevents the user from calling this function when
-- the player is offline, unlike the _real_ formspec submission.
local player = minetest.get_player_by_name(name)
if not player then
minetest.log("warning", "[flow] Player " .. name ..
" was offline when render_to_formspec_string event was" ..
" triggered. Events were not passed through.")
return nil
end
return fs_process_events(player, form_info, fields)
end
return fs, event, public_form_info
end
function Form:close(player)
local name = player:get_player_name()
local form_info = open_formspecs[name]
@ -894,12 +935,8 @@ function flow.make_gui(build_func)
return setmetatable({_build = build_func}, form_mt)
end
local function on_fs_input(player, formname, fields)
local name = player:get_player_name()
local form_infos = formname == "" and open_inv_formspecs or open_formspecs
local form_info = form_infos[name]
if not form_info or formname ~= form_info.formname then return end
-- Declared locally above to be accessible to render_to_formspec_string
function fs_process_events(player, form_info, fields)
local callbacks = form_info.callbacks
local ctx = form_info.ctx
local redraw_if_changed = form_info.redraw_if_changed
@ -915,6 +952,7 @@ local function on_fs_input(player, formname, fields)
-- large amount of data and very long strings have the
-- potential to break things. Please open an issue if you
-- (somehow) need to use longer text in fields.
local name = player:get_player_name()
minetest.log("warning", "[flow] Player " .. name .. " tried" ..
" submitting a large field value (>60 kB), ignoring.")
else
@ -922,7 +960,7 @@ local function on_fs_input(player, formname, fields)
if ctx_form[field] ~= new_value then
if redraw_if_changed[field] then
redraw_fs = true
elseif formname == "" then
elseif form_info.formname == "" then
-- Update the inventory when the player closes it next
form_info.ctx_form_modified = true
end
@ -939,6 +977,17 @@ local function on_fs_input(player, formname, fields)
end
end
return redraw_fs
end
local function on_fs_input(player, formname, fields)
local name = player:get_player_name()
local form_infos = formname == "" and open_inv_formspecs or open_formspecs
local form_info = form_infos[name]
if not form_info or formname ~= form_info.formname then return end
local redraw_fs = fs_process_events(player, form_info, fields)
if form_infos[name] ~= form_info then return true end
if formname == "" then
@ -958,6 +1007,7 @@ local function on_leaveplayer(player)
local name = player:get_player_name()
open_formspecs[name] = nil
open_inv_formspecs[name] = nil
render_to_formspec_auto_name_ids[name] = nil
end
if DEBUG_MODE then

View File

@ -72,7 +72,7 @@ local gui = flow.widgets
-- values and fix weird floating point offsets
local function normalise_tree(tree)
tree = formspec_ast.flatten(tree)
tree.formspec_version = 5
tree.formspec_version = 6
return assert(formspec_ast.parse(formspec_ast.unparse(tree)))
end
@ -282,7 +282,7 @@ describe("Flow", function()
it("registers inventory formspecs", function ()
local stupid_simple_inv_expected =
"formspec_version[5]" ..
"formspec_version[6]" ..
"size[10.35,5.35]" ..
"list[current_player;main;0.3,0.3;8,4]"
local stupid_simple_inv = flow.make_gui(function (p, c)
@ -300,7 +300,7 @@ describe("Flow", function()
end)
it("can still show a form when an inventory formspec is shown", function ()
local expected_one = "formspec_version[5]size[1.6,1.6]box[0.3,0.3;1,1;]"
local expected_one = "formspec_version[6]size[1.6,1.6]box[0.3,0.3;1,1;]"
local one = flow.make_gui(function (p, c)
return gui.Box{ w = 1, h = 1 }
end)
@ -314,4 +314,71 @@ describe("Flow", function()
blue:show(player)
assert(player:get_inventory_formspec() == expected_one)
end)
describe("render_to_formspec_string", function ()
it("renders the same output as manually calling _render when standalone", function()
local build_func = function()
return gui.VBox{
gui.Box{w = 1, h = 1},
gui.Label{label = "Test", align_h = "centre"},
gui.Field{name = "4", label = "Test", align_v = "fill"}
}
end
local form = flow.make_gui(build_func)
local player = stub_player("test_player")
local fs = form:render_to_formspec_string(player, nil, true)
test_render(build_func, fs)
end)
it("renders nearly the same output as manually calling _render when not standalone", function()
local build_func = function()
return gui.VBox{
gui.Box{w = 1, h = 1},
gui.Label{label = "Test", align_h = "centre"},
gui.Field{name = "4", label = "Test", align_v = "fill"}
}
end
local form = flow.make_gui(build_func)
local player = stub_player("test_player")
local fs, _, info = form:render_to_formspec_string(player)
test_render(
build_func,
("formspec_version[%s]size[%s,%s]"):format(
info.formspec_version,
info.w,
info.h
) .. fs
)
end)
it("passes events through the callback function", function()
local manual_spy
local manual_spy_count = 0
local buttonargs = {
label = "Click me!",
name = "btn",
on_event = function (...)
manual_spy = {...}
manual_spy_count = manual_spy_count + 1
end
}
local form = flow.make_gui(function()
return gui.Button(buttonargs)
end)
local player = stub_player("test_player")
function minetest.get_player_by_name(name)
assert(name == "test_player")
return player
end
local ctx = {a = 1}
local _, trigger_event = form:render_to_formspec_string(player, ctx, true)
local fields = {btn = 1}
trigger_event(fields)
assert.equals(manual_spy_count, 1, "event passed down only once")
assert.equals(manual_spy[1], player, "player was first arg")
assert.equals(manual_spy[2], ctx, "context was next")
minetest.get_player_by_name = nil
end)
end)
end)