From 60d2674a457d89fa7d7388e8a4a924fc3c6c97a9 Mon Sep 17 00:00:00 2001 From: Lazerbeak12345 Date: Fri, 7 Apr 2023 19:13:44 -0600 Subject: [PATCH] feat: Add render_to_formspec_string function (#5) Co-authored-by: luk3yx --- README.md | 41 +++++++++++++++++++++++++------ init.lua | 64 ++++++++++++++++++++++++++++++++++++++++++------ test.lua | 73 ++++++++++++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 161 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index fdd17fe..d46d120 100644 --- a/README.md +++ b/README.md @@ -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. + +
+no_prepend[] 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 +
+Using a form as an inventory 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. + +
+Rendering to a formspec + +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!** + +
diff --git a/init.lua b/init.lua index 62ba2dc..c76c49c 100644 --- a/init.lua +++ b/init.lua @@ -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 diff --git a/test.lua b/test.lua index 134c81b..c5585ca 100644 --- a/test.lua +++ b/test.lua @@ -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)