feat: Embed support (#15)
Co-authored-by: luk3yx <luk3yx@users.noreply.github.com>
This commit is contained in:
parent
da87496e26
commit
1528e87864
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,2 +1,3 @@
|
|||||||
test-fs.lua
|
test-fs.lua
|
||||||
*.old
|
*.old
|
||||||
|
luacov.*.out
|
||||||
|
92
embed.lua
Normal file
92
embed.lua
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
local embed_create_ctx_mt = {}
|
||||||
|
|
||||||
|
function embed_create_ctx_mt:__index(key)
|
||||||
|
-- rawget ensures we don't do recursion
|
||||||
|
local form = rawget(self, "_flow_embed_parent_form")
|
||||||
|
local prefix = rawget(self, "_flow_embed_prefix")
|
||||||
|
return form[prefix .. key]
|
||||||
|
end
|
||||||
|
|
||||||
|
function embed_create_ctx_mt:__newindex(key, value)
|
||||||
|
local form = rawget(self, "_flow_embed_parent_form")
|
||||||
|
local prefix = rawget(self, "_flow_embed_prefix")
|
||||||
|
form[prefix .. key] = value
|
||||||
|
end
|
||||||
|
|
||||||
|
local function embed_create_ctx(ctx, name, prefix)
|
||||||
|
if not ctx[name] then
|
||||||
|
ctx[name] = {}
|
||||||
|
end
|
||||||
|
if not ctx[name].form then
|
||||||
|
ctx[name].form = {}
|
||||||
|
end
|
||||||
|
if getmetatable(ctx[name].form) ~= embed_create_ctx_mt then
|
||||||
|
ctx[name].form._flow_embed_prefix = prefix
|
||||||
|
ctx[name].form._flow_embed_parent_form = ctx.form
|
||||||
|
ctx[name].form = setmetatable(ctx[name].form, embed_create_ctx_mt)
|
||||||
|
end
|
||||||
|
return ctx[name]
|
||||||
|
end
|
||||||
|
|
||||||
|
local function embed_wrap_callback_func(func, name, prefix)
|
||||||
|
return function(player, ctx)
|
||||||
|
return func(player, embed_create_ctx(ctx, name, prefix))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function embed_add_prefix(node, name, prefix)
|
||||||
|
if node.type == "style" and node.selectors then
|
||||||
|
-- Add prefix to style[] selectors
|
||||||
|
for i, selector in ipairs(node.selectors) do
|
||||||
|
node.selectors[i] = prefix .. selector
|
||||||
|
end
|
||||||
|
elseif node.type == "scroll_container" and node.scrollbar_name then
|
||||||
|
node.scrollbar_name = prefix .. node.scrollbar_name
|
||||||
|
elseif node.type == "tooltip" and node.gui_element_name then
|
||||||
|
node.gui_element_name = prefix .. node.gui_element_name
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Add prefix to all names
|
||||||
|
if node.name then
|
||||||
|
node.name = prefix .. node.name
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Wrap callback functions
|
||||||
|
if node.on_event then
|
||||||
|
node.on_event = embed_wrap_callback_func(node.on_event, name, prefix)
|
||||||
|
end
|
||||||
|
if node.on_quit then
|
||||||
|
node.on_quit = embed_wrap_callback_func(node.on_quit, name, prefix)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Recurse to child nodes
|
||||||
|
for _, child in ipairs(node) do
|
||||||
|
embed_add_prefix(child, name, prefix)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- TODO: Unit test this
|
||||||
|
local change_ctx = ...
|
||||||
|
|
||||||
|
return function(self, fields)
|
||||||
|
local player = fields.player
|
||||||
|
local name = fields.name
|
||||||
|
-- TODO: It might be cool to somehow pass elements down (number-indexes
|
||||||
|
-- of fields) into the child form, but I'm not sure how that would look
|
||||||
|
-- on the form definition side.
|
||||||
|
-- Perhaps passing it in via the context, or an extra arg to _build?
|
||||||
|
local parent_ctx = flow.get_context()
|
||||||
|
if name == nil then
|
||||||
|
-- Don't prefix anything if name is unspecified
|
||||||
|
return self._build(player, parent_ctx)
|
||||||
|
end
|
||||||
|
|
||||||
|
local prefix = "\2" .. name .. "\2"
|
||||||
|
local child_ctx = embed_create_ctx(parent_ctx, name, prefix)
|
||||||
|
change_ctx(child_ctx)
|
||||||
|
local root_node = self._build(player, child_ctx)
|
||||||
|
change_ctx(parent_ctx)
|
||||||
|
|
||||||
|
embed_add_prefix(root_node, name, prefix)
|
||||||
|
return root_node
|
||||||
|
end
|
6
init.lua
6
init.lua
@ -20,6 +20,7 @@
|
|||||||
local DEBUG_MODE = false
|
local DEBUG_MODE = false
|
||||||
flow = {}
|
flow = {}
|
||||||
local S = minetest.get_translator("flow")
|
local S = minetest.get_translator("flow")
|
||||||
|
local modpath = minetest.get_modpath("flow")
|
||||||
|
|
||||||
local Form = {}
|
local Form = {}
|
||||||
|
|
||||||
@ -1229,6 +1230,10 @@ function Form:update_where(func)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
Form.embed = assert(loadfile(modpath .. "/embed.lua"))(function(new_context)
|
||||||
|
current_ctx = new_context
|
||||||
|
end)
|
||||||
|
|
||||||
local form_mt = {__index = Form}
|
local form_mt = {__index = Form}
|
||||||
function flow.make_gui(build_func)
|
function flow.make_gui(build_func)
|
||||||
return setmetatable({_build = build_func}, form_mt)
|
return setmetatable({_build = build_func}, form_mt)
|
||||||
@ -1509,7 +1514,6 @@ function gui_mt.__newindex()
|
|||||||
error("Cannot modifiy gui table")
|
error("Cannot modifiy gui table")
|
||||||
end
|
end
|
||||||
|
|
||||||
local modpath = minetest.get_modpath("flow")
|
|
||||||
if minetest.is_singleplayer() then
|
if minetest.is_singleplayer() then
|
||||||
local example_form
|
local example_form
|
||||||
minetest.register_chatcommand("flow-example", {
|
minetest.register_chatcommand("flow-example", {
|
||||||
|
242
test.lua
242
test.lua
@ -20,11 +20,18 @@ end
|
|||||||
|
|
||||||
local function dummy() end
|
local function dummy() end
|
||||||
minetest.register_on_leaveplayer = dummy
|
minetest.register_on_leaveplayer = dummy
|
||||||
minetest.get_modpath = dummy
|
|
||||||
minetest.is_singleplayer = dummy
|
minetest.is_singleplayer = dummy
|
||||||
minetest.get_player_information = dummy
|
minetest.get_player_information = dummy
|
||||||
minetest.show_formspec = dummy
|
minetest.show_formspec = dummy
|
||||||
|
|
||||||
|
function minetest.get_modpath(modname)
|
||||||
|
if modname == "flow" then
|
||||||
|
return "."
|
||||||
|
elseif modname == "formspec_ast" then
|
||||||
|
return FORMSPEC_AST_PATH
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
function minetest.get_translator(modname)
|
function minetest.get_translator(modname)
|
||||||
assert(modname == "flow")
|
assert(modname == "flow")
|
||||||
return function(str) return str end
|
return function(str) return str end
|
||||||
@ -109,16 +116,25 @@ local function render(build_func, ctx, fs_ver)
|
|||||||
return form:_render({get_player_name = "test"}, ctx or {}, fs_ver)
|
return form:_render({get_player_name = "test"}, ctx or {}, fs_ver)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function test_render(build_func, output)
|
local function test_render(build_func, output, description)
|
||||||
local tree = render(build_func)
|
local tree = render(build_func)
|
||||||
local expected_tree = assert(formspec_ast.parse(output))
|
local expected_tree = output
|
||||||
|
if type(output) == "string" then
|
||||||
assert.same(normalise_tree(expected_tree), normalise_tree(tree))
|
expected_tree = assert(formspec_ast.parse(output), "expected output must parse")
|
||||||
|
end
|
||||||
|
if expected_tree.type then
|
||||||
|
expected_tree = assert(render(expected_tree), "if expected output is a flow form, it must render")
|
||||||
|
end
|
||||||
|
tree = normalise_tree(tree)
|
||||||
|
expected_tree = normalise_tree(expected_tree)
|
||||||
|
assert.same(expected_tree, tree, description)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function render_to_string(tree)
|
local function render_to_string(tree)
|
||||||
local player = stub_player("test_player")
|
local player = stub_player("test_player")
|
||||||
local form = flow.make_gui(function() return table.copy(tree) end)
|
local form = flow.make_gui(function()
|
||||||
|
return table.copy(tree)
|
||||||
|
end)
|
||||||
local ctx = {}
|
local ctx = {}
|
||||||
local _, event = form:render_to_formspec_string(player, ctx)
|
local _, event = form:render_to_formspec_string(player, ctx)
|
||||||
return ctx, event
|
return ctx, event
|
||||||
@ -903,4 +919,218 @@ describe("Flow", function()
|
|||||||
]])
|
]])
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
describe("Flow.embed", function()
|
||||||
|
local embedded_form = flow.make_gui(function(_, x)
|
||||||
|
return gui.VBox{
|
||||||
|
gui.Label{label = "This is the embedded form!"},
|
||||||
|
gui.Field{name = "test2"},
|
||||||
|
x.a and gui.Label{label = "A is true!" .. x.a} or gui.Nil{}
|
||||||
|
}
|
||||||
|
end)
|
||||||
|
it("raises an error if called outside of a form context", function()
|
||||||
|
assert.has_error(function()
|
||||||
|
embedded_form:embed{
|
||||||
|
-- It's fully possible that the API user would have access
|
||||||
|
-- to a player reference
|
||||||
|
player = stub_player"test_player",
|
||||||
|
name = "theprefix"
|
||||||
|
}
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
it("returns a flow widget", function ()
|
||||||
|
test_render(function(p, _)
|
||||||
|
return gui.HBox{
|
||||||
|
gui.Label{label = "asdft"},
|
||||||
|
embedded_form:embed{player = p, name = "theprefix"},
|
||||||
|
gui.Label{label = "ffaksksdf"}
|
||||||
|
}
|
||||||
|
end, gui.HBox{
|
||||||
|
gui.Label{label = "asdft"},
|
||||||
|
gui.VBox{
|
||||||
|
gui.Label{label = "This is the embedded form!"},
|
||||||
|
-- The exact prefix is an implementation detail, you
|
||||||
|
-- shouldn't rely on this in your own code
|
||||||
|
gui.Field{name = "\2theprefix\2test2"},
|
||||||
|
},
|
||||||
|
gui.Label{label = "ffaksksdf"}
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
it("supports nil prefix", function()
|
||||||
|
test_render(function(p, _)
|
||||||
|
return gui.HBox{
|
||||||
|
gui.Label{label = "asdft"},
|
||||||
|
embedded_form:embed{player = p},
|
||||||
|
gui.Label{label = "ffaksksdf"}
|
||||||
|
}
|
||||||
|
end, gui.HBox{
|
||||||
|
gui.Label{label = "asdft"},
|
||||||
|
gui.VBox{
|
||||||
|
gui.Label{label = "This is the embedded form!"},
|
||||||
|
gui.Field{name = "test2"},
|
||||||
|
},
|
||||||
|
gui.Label{label = "ffaksksdf"}
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
it("child context object lives inside the host", function()
|
||||||
|
test_render(function(p, x)
|
||||||
|
assert.Nil(
|
||||||
|
x.theprefix,
|
||||||
|
"Prefixes are inserted when :embed is called. "..
|
||||||
|
"The first time this renders, it hasn't been called yet."
|
||||||
|
)
|
||||||
|
-- Technically, that means both of these will be true the first time
|
||||||
|
-- This code only ever runs once, so that's every time.
|
||||||
|
-- Regardless, this is how ordinary API users would be using it.
|
||||||
|
if not x.theprefix then
|
||||||
|
x.theprefix = {}
|
||||||
|
end
|
||||||
|
if not x.theprefix.a then
|
||||||
|
x.theprefix.a = " WOW!"
|
||||||
|
end
|
||||||
|
return gui.HBox{
|
||||||
|
gui.Label{label = "asdft"},
|
||||||
|
embedded_form:embed{player = p, name = "theprefix"},
|
||||||
|
gui.Label{label = "ffaksksdf"}
|
||||||
|
}
|
||||||
|
end, gui.HBox{
|
||||||
|
gui.Label{label = "asdft"},
|
||||||
|
gui.VBox{
|
||||||
|
gui.Label{label = "This is the embedded form!"},
|
||||||
|
gui.Field{name = "\2theprefix\2test2"},
|
||||||
|
gui.Label{label = "A is true! WOW!"}
|
||||||
|
},
|
||||||
|
gui.Label{label = "ffaksksdf"}
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
it("flow form context table", function()
|
||||||
|
test_render(function(p, x)
|
||||||
|
x.form["\2the_name\2jkl"] = 3
|
||||||
|
local child = flow.make_gui(function(_p, xc)
|
||||||
|
xc.form.thingy = true
|
||||||
|
xc.form.jkl = 9
|
||||||
|
return gui.Label{label = "asdf"}
|
||||||
|
end):embed{
|
||||||
|
player = p,
|
||||||
|
name = "the_name"
|
||||||
|
}
|
||||||
|
assert.True(x.form["\2the_name\2thingy"])
|
||||||
|
assert.equal(9, x.form["\2the_name\2jkl"])
|
||||||
|
return child
|
||||||
|
end, gui.Label{label = "asdf"})
|
||||||
|
end)
|
||||||
|
it("host may modify the returned flow form", function()
|
||||||
|
test_render(function(p, _x)
|
||||||
|
local e = embedded_form:embed{player = p, name = "asdf"}
|
||||||
|
e[#e+1] = gui.Box{w = 1, h = 3}
|
||||||
|
return e
|
||||||
|
end, gui.VBox{
|
||||||
|
gui.Label{label = "This is the embedded form!"},
|
||||||
|
gui.Field{name = "\2asdf\2test2"},
|
||||||
|
gui.Box{w = 1, h = 3}
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
it("event handler called correctly", function()
|
||||||
|
local function func_btn_event() end
|
||||||
|
local function func_field_event() return true end
|
||||||
|
local function func_quit() end
|
||||||
|
|
||||||
|
func_btn_event = spy.new(func_btn_event)
|
||||||
|
func_field_event = spy.new(func_field_event)
|
||||||
|
func_quit = spy.new(func_quit)
|
||||||
|
|
||||||
|
local wrapped_p, wrapped_x
|
||||||
|
local event_embedded_form = flow.make_gui(function(p, x)
|
||||||
|
wrapped_p, wrapped_x = p, x
|
||||||
|
return gui.VBox{
|
||||||
|
on_quit = func_quit,
|
||||||
|
gui.Label{label = "Callback demo:"},
|
||||||
|
gui.Button{label = "Click me!", name = "btn", on_event = func_btn_event},
|
||||||
|
gui.Field{name = "field", on_event = func_field_event}
|
||||||
|
}
|
||||||
|
end)
|
||||||
|
|
||||||
|
local _tree, state = render(function(player, _ctx)
|
||||||
|
return event_embedded_form:embed{
|
||||||
|
player = player,
|
||||||
|
name = "thesubform"
|
||||||
|
}
|
||||||
|
end)
|
||||||
|
|
||||||
|
local player, ctx = wrapped_p, state.ctx
|
||||||
|
state.callbacks.quit(player, ctx)
|
||||||
|
state.callbacks["\2thesubform\2field"](player, ctx)
|
||||||
|
state.btn_callbacks["\2thesubform\2btn"](player, ctx)
|
||||||
|
|
||||||
|
assert.same(state.ctx.thesubform, wrapped_x)
|
||||||
|
|
||||||
|
assert.spy(func_quit).was.called(1)
|
||||||
|
assert.spy(func_quit).was.called_with(player, wrapped_x)
|
||||||
|
assert.spy(func_field_event).was.called(1)
|
||||||
|
assert.spy(func_field_event).was.called_with(player, wrapped_x)
|
||||||
|
assert.spy(func_btn_event).was.called(1)
|
||||||
|
assert.spy(func_btn_event).was.called_with(player, wrapped_x)
|
||||||
|
|
||||||
|
-- Each of these are wrapped with another function to put the actual function in the correct environment
|
||||||
|
assert.Not.same(func_quit, state.callbacks.quit)
|
||||||
|
assert.Not.same(func_field_event, state.callbacks["\2thesubform\2field"])
|
||||||
|
assert.Not.same(func_btn_event, state.callbacks["\2thesubform\2btn"])
|
||||||
|
end)
|
||||||
|
describe("metadata", function()
|
||||||
|
it("style data is modified", function()
|
||||||
|
local style_embedded_form = flow.make_gui(function (p, x)
|
||||||
|
return gui.VBox{
|
||||||
|
gui.Style{selectors = {"test"}, props = {prop = "value"}},
|
||||||
|
}
|
||||||
|
end)
|
||||||
|
test_render(function(p, _x)
|
||||||
|
return style_embedded_form:embed{player = p, name = "asdf"}
|
||||||
|
end, gui.VBox{
|
||||||
|
gui.Style{selectors = {"\2asdf\2test"}, props = {prop = "value"}},
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
it("scroll_container data is modified", function()
|
||||||
|
local scroll_embedded_form = flow.make_gui(function(p, x)
|
||||||
|
return gui.VBox{
|
||||||
|
gui.ScrollContainer{scrollbar_name = "name"}
|
||||||
|
}
|
||||||
|
end)
|
||||||
|
test_render(function(p, _x)
|
||||||
|
return scroll_embedded_form:embed{player = p, name = "asdf"}
|
||||||
|
end, gui.VBox{
|
||||||
|
gui.ScrollContainer{scrollbar_name = "\2asdf\2name"}
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
it("tooltip data is modified", function()
|
||||||
|
local tooltip_embedded_form = flow.make_gui(function(p, x)
|
||||||
|
return gui.VBox{
|
||||||
|
gui.Tooltip{gui_element_name = "lololol"}
|
||||||
|
}
|
||||||
|
end)
|
||||||
|
test_render(function(p, _x)
|
||||||
|
return tooltip_embedded_form:embed{player = p, name = "asdf"}
|
||||||
|
end, gui.VBox{
|
||||||
|
gui.Tooltip{gui_element_name = "\2asdf\2lololol"}
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
it("supports fresh initial form values", function()
|
||||||
|
local tooltip_embedded_form = flow.make_gui(function(p, x)
|
||||||
|
assert.same("initial value!", x.field)
|
||||||
|
return gui.VBox{
|
||||||
|
gui.Field{name = "field"}
|
||||||
|
}
|
||||||
|
end)
|
||||||
|
test_render(function(p, x)
|
||||||
|
if not x.asdf then
|
||||||
|
x.asdf = {
|
||||||
|
field = "initial value!"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
return tooltip_embedded_form:embed{player = p, name = "asdf"}
|
||||||
|
end, gui.VBox{
|
||||||
|
gui.Field{name = "\2asdf\2field"}
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
end)
|
||||||
end)
|
end)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user