Add experimental low-level popover API

This commit is contained in:
luk3yx 2025-08-13 16:48:17 +12:00
parent d7c6b66969
commit 1960dbd502
5 changed files with 177 additions and 7 deletions

82
doc/popovers.md Normal file
View File

@ -0,0 +1,82 @@
# Popovers (highly experimental)
**This API will likely have breaking changes in the future.**
Flow supports a highly experimental low-level API for defining popovers.
Popovers appear over top of all other elements and are anchored to their parent.
This API is intended to be used for creating themed widgets like dropdowns, or
for a higher level popover API that adds styling and is easier to use.
You add a `popover` attribute to any container element to define a popover.
Flow does not manage the lifecycle (opening/closing) of popovers, you must
ensure that you do this yourself like in the example.
See the below example for details on how to use the API:
```lua
gui.Stack{
gui.Button{
label = "Open popover",
-- When the popover button is clicked, open the popover
on_event = function(player, ctx)
ctx.show_popover = true
return true
end,
},
-- The actual element to overlay
-- "popover" is set to the gui.VBox element if (and only if)
-- ctx.show_popover is true, otherwise it's nil so no popover is shown.
popover = ctx.show_popover and gui.VBox{
-- Specifying padding and bgcolor is optional, but is probably a good
-- idea since flow doesn't do any styling on its own.
padding = 0.2,
bgcolor = "#222e",
-- "anchor" specifies how the popover is positioned relative to the
-- parent, and can be "bottom" (default), "top", "left", or "right".
anchor = ctx.form.anchor,
-- align_h and align_v align the popover according to its parent
-- element. You only need to specify align_h for
-- anchor = "top"/"bottom" or align_v for anchor = "left"/"right", this
-- example specifies both so that it can demonstrate switching between
-- different anchor types.
align_h = "fill",
align_v = "center",
-- Popover contents
gui.Label{label = "Hi there!"},
gui.Dropdown{
name = "anchor",
items = {
"bottom",
"top",
"left",
"right",
},
},
} or nil,
-- Clicking outside the popover will call this function, which should
-- probably close the popover.
on_close_popover = function(player, ctx)
ctx.show_popover = false
return true
end,
}
```
There are some restrictions on popovers:
- They must not extend outside the form, otherwise they'll only be partially
visible (unless everything is styled with `noclip = true`). Flow does not
attempt to detect this.
- You can only define `popover` on container elements, like gui.Stack,
gui.HBox, and gui.VBox.
- Only one popover is shown at a time.
- Players can still use tab to interact with things behind the popover,
despite being unable to use their mouse to do so.
- You cannot show popovers inside of other popovers.

View File

@ -17,8 +17,8 @@
-- along with this program. If not, see <https://www.gnu.org/licenses/>.
--
local DEFAULT_SPACING, LABEL_HEIGHT, get_and_fill_in_sizes,
invisible_elems = ...
local DEFAULT_SPACING, LABEL_HEIGHT, apply_padding, get_and_fill_in_sizes,
invisible_elems, modpath = ...
local align_types = {}
function align_types.fill(node, x, w, extra_space)
@ -132,7 +132,7 @@ function align_types.auto(node, x, w, extra_space, cross)
end
end
local expand_child_boxes
local expand_child_boxes, handle_popovers
local function expand(box)
local x, w, align_h, y, h, align_v
local box_type = box.type
@ -164,6 +164,7 @@ local function expand(box)
node.w = box.w
end
expand(node)
handle_popovers(box, node)
end
return
elseif box_type == "padding" then
@ -256,7 +257,13 @@ function expand_child_boxes(box)
else
expand(node)
end
handle_popovers(box, node)
end
end
handle_popovers = assert(loadfile(modpath .. "/popover.lua"))(
align_types, apply_padding, get_and_fill_in_sizes, expand
)
return expand

View File

@ -26,7 +26,8 @@ local apply_padding, get_and_fill_in_sizes, set_current_lang,
dofile(modpath .. "/layout.lua")
local expand = assert(loadfile(modpath .. "/expand.lua"))(
DEFAULT_SPACING, LABEL_HEIGHT, get_and_fill_in_sizes, invisible_elems
DEFAULT_SPACING, LABEL_HEIGHT, apply_padding, get_and_fill_in_sizes,
invisible_elems, modpath
)
local parse_callbacks = dofile(modpath .. "/input.lua")
@ -107,6 +108,25 @@ local function render_ast(node, embedded)
end
res[#res + 1] = node
if node.popover and node.type == "container" then
node.popover.x = node.popover.x + node.x
node.popover.y = node.popover.y + node.y
res[#res + 1] = {
type = "image_button",
x = -99,
y = -99,
w = 999,
h = 999,
drawborder = false,
noclip = true,
on_event = node.on_close_popover,
}
res[#res + 1] = node.popover
assert(not node.popover.popover,
"Nested popovers are not supported")
end
return res
end
@ -266,9 +286,6 @@ function Form:_render(player, ctx, formspec_version, id1, embedded, lang_code)
current_player = player
current_ctx = ctx
local box = self._build(player, ctx)
current_player = nil
current_ctx = nil
gui.formspec_version = 0
-- Restore the original ctx.form
assert(ctx.form == wrapped_form,
@ -280,6 +297,11 @@ function Form:_render(player, ctx, formspec_version, id1, embedded, lang_code)
if not id1 or id1 > 1e6 then id1 = 0 end
local tree = render_ast(box, embedded)
current_player = nil
current_ctx = nil
gui.formspec_version = 0
local callbacks, btn_callbacks, saved_fields, id2, on_key_enters =
parse_callbacks(tree, orig_form, id1, embedded, formspec_version)

View File

@ -18,6 +18,7 @@ nav:
- inventory.md
- manual-positioning.md
- experimental.md
- popovers.md
- Development tools:
- Flow Inspector: https://content.luanti.org/packages/luk3yx/flow_inspector/
- Flow Playground / Tutorial: https://luk3yx.gitlab.io/minetest-flow-playground/

58
popover.lua Normal file
View File

@ -0,0 +1,58 @@
local align_types, apply_padding, get_and_fill_in_sizes, expand = ...
local function handle_popovers(box, node)
local popover = node.popover
if not popover then
return
end
-- Copy popovers to their parent
assert(node.type == "container" or node.type == "scroll_container",
"Popovers are currently only supported on container elements")
local offset_x, offset_y = node.x, node.y
if not popover._flow_popover_root then
local p_w, p_h = apply_padding(popover, 0, 0)
local n_w, n_h = get_and_fill_in_sizes(node)
if popover.anchor == "top" then
offset_y = offset_y - p_h
elseif popover.anchor == "left" then
offset_x = offset_x - p_w
elseif popover.anchor == "right" then
offset_x = offset_x + n_w
else
offset_y = offset_y + n_h
end
if popover.anchor == "left" or popover.anchor == "right" then
align_types[popover.align_v or "auto"](popover, "y", "h", n_h - p_h)
else
align_types[popover.align_h or "auto"](popover, "x", "w", n_w - p_w)
end
popover._flow_popover_root = true
end
if node.type == "scroll_container" then
local ctx = flow.get_context()
local offset = (ctx.form[node.scrollbar_name] or 0) * (node.scroll_factor or 0.1)
if node.orientation == "horizontal" then
offset_x = offset_x - offset
else
offset_y = offset_y - offset
end
end
popover.x = popover.x + offset_x
popover.y = popover.y + offset_y
box.popover = popover
box.on_close_popover = node.on_close_popover
expand(popover)
popover = nil -- Reduce the impact of API misuse
node.on_close_popover = nil
end
return handle_popovers