diff --git a/README.md b/README.md
index ae1ba33..1b98b4f 100644
--- a/README.md
+++ b/README.md
@@ -377,7 +377,52 @@ it is 0.1.
## Styling forms
-To style forms, you use the `gui.Style` and `gui.StyleType` elements:
+### Experimental new syntax
+
+At the moment I suggest only using this syntax if your form won't look broken
+without the style - older versions of flow don't support this syntax, and I may
+make breaking changes to it (such as sub-style syntax) in the future.
+
+You can add inline styles to elements with the `style` field:
+
+```lua
+gui.Button{
+ label = "Test",
+ style = {
+ bgcolor = "red",
+
+ -- You can style specific states of elements:
+ {sel = "$hovered", bgcolor = "green"},
+
+ -- Or a combination of states:
+ {sel = "$hovered, $pressed", bgcolor = "blue"},
+ {sel = "$hovered+pressed", bgcolor = "white"},
+ },
+}
+```
+
+If you need to style multiple elements, you can reuse the `style` table:
+
+```lua
+local my_style = {bgcolor = "red", {sel = "$hovered", bgcolor = "green"}}
+
+local gui = flow.make_gui(function(player, ctx)
+ return gui.VBox{
+ gui.Button{label = "Styled button", style = my_style},
+ gui.Button{label = "Unstyled button"},
+ gui.Button{label = "Second styled button", style = my_style},
+ }
+end)
+```
+
+Note that this may inadvertently reset styles on subsequent elements if used on
+elements without a name due to formspec limitations.
+
+### Alternative more stable syntax
+
+Alternatively, you can use the `gui.Style` and `gui.StyleType` elements if you
+need to style a large group of elements or need to support older versions of
+flow:
```lua
gui.Style{
@@ -394,15 +439,74 @@ gui.Button{
},
```
-The style elements are invisible and won't affect padding.
+The `Style` and `StyleType` elements are invisible and won't affect padding.
-## Hiding elements
+## Other features
+
+
+Tooltips
+
+You can add tooltips to elements using the `tooltip` field:
+
+```lua
+gui.Image{
+ w = 2, h = 2,
+ texture_name = "air.png",
+ tooltip = "Air",
+}
+```
+
+
+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.
-## Experimental features
+
+Using a form as an inventory
+
+> [!TIP]
+> Consider using [Sway](https://content.minetest.net/packages/lazerbeak12345/sway/)
+> instead if you want to use flow as an inventory replacement while still
+> having some way for other mods to extend the 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
+completely replace your inventory and isn't compatible with inventory mods like
+sfinv.
+
+```lua
+local example_inventory = flow.make_gui(function (player, context)
+ return gui.Label{ label = "Inventory goes here!" }
+end)
+minetest.register_on_joinplayer(function(player)
+ example_inventory:set_as_inventory_for(player)
+end)
+```
+
+Like with the `show_hud` function, `update*` functions don't do anything, so to
+update it, call `set_as_inventory_for` again with the new context. If the
+context is not provided, it will reuse the existing context.
+
+```lua
+example_inventory:set_as_inventory_for(player, new_context)
+```
+
+While the form will of course be cleared when the player leaves, if you'd like
+to unset the inventory manually, call `:unset_as_inventory_for(player)`,
+analogue to `close_hud`:
+
+```lua
+example_inventory:unset_as_inventory_for(player)
+```
+
+This will set the inventory formspec string to `""` and stop flow from
+processing inventory formspec input.
+
+
+
+### Experimental features
These features might be broken in the future.
@@ -438,42 +542,6 @@ You can set `bgcolor = "#123"`, `fbgcolor = "#123"`, and
`bg_fullscreen = true` on the root element to set a background colour. The
values for these correspond to the [`bgcolor` formspec element](https://minetest.gitlab.io/minetest/formspec/#bgcolorbgcolorfullscreenfbgcolor).
-
-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
-completely replace your inventory and isn't compatible with inventory mods like
-sfinv.
-
-```lua
-local example_inventory = flow.make_gui(function (player, context)
- return gui.Label{ label = "Inventory goes here!" }
-end)
-minetest.register_on_joinplayer(function(player)
- example_inventory:set_as_inventory_for(player)
-end)
-```
-
-Like with the `show_hud` function, `update*` functions don't do anything, so to
-update it, call `set_as_inventory_for` again with the new context. If the
-context is not provided, it will reuse the existing context.
-
-```lua
-example_inventory:set_as_inventory_for(player, new_context)
-```
-
-While the form will of course be cleared when the player leaves, if you'd like
-to unset the inventory manually, call `:unset_as_inventory_for(player)`,
-analogue to `close_hud`:
-
-```lua
-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
@@ -495,6 +563,7 @@ call `form:render_to_formspec_string(player, ctx, standalone)`.
even if fields.quit is sent.
-**Do not use this API with node meta formspecs, it can and will break!**
+> [!CAUTION]
+> Do not use this API with node meta formspecs, it can and will break!
diff --git a/init.lua b/init.lua
index 32f3602..f8858a3 100644
--- a/init.lua
+++ b/init.lua
@@ -338,6 +338,7 @@ function align_types.fill(node, x, w, extra_space)
x = 0, y = 0,
w = node.w + extra_space, h = node.h,
name = "\1", label = node.label,
+ style = node.style,
}
-- Overlay button to prevent clicks from doing anything
@@ -352,6 +353,7 @@ function align_types.fill(node, x, w, extra_space)
node.y = node.y - LABEL_OFFSET
node.label = nil
+ node.style = nil
node._label_hack = true
assert(#node == 4)
end
@@ -901,6 +903,104 @@ function flow.get_context()
return current_ctx
end
+local function insert_style_elem(tree, i, node, props, sels)
+ local base_selector = node.name or node.type
+ local selectors = {}
+ if sels then
+ for i, sel in ipairs(sels) do
+ local suffix = sel:match("^%s*$(.-)%s*$")
+ if suffix then
+ selectors[i] = base_selector .. ":" .. suffix
+ else
+ minetest.log("warning", "[flow] Invalid style selector: " ..
+ tostring(sel))
+ end
+ end
+ else
+ selectors[1] = base_selector
+ end
+
+
+ table.insert(tree, i, {
+ type = node.name and "style" or "style_type",
+ selectors = selectors,
+ props = props,
+ })
+
+ if not node.name then
+ -- Undo style_type modifications
+ local reset_props = {}
+ for k in pairs(props) do
+ -- The style table might have substyles which haven't been removed
+ -- yet
+ reset_props[k] = ""
+ end
+
+ table.insert(tree, i + 2, {
+ type = "style_type",
+ selectors = selectors,
+ props = reset_props,
+ })
+ end
+end
+
+local function extract_props(t)
+ local res = {}
+ for k, v in pairs(t) do
+ if k ~= "sel" and type(k) == "string" then
+ res[k] = v
+ end
+ end
+ return res
+end
+
+-- I don't like the idea of making yet another pass over the element tree but I
+-- can't think of a clean way of integrating shorthand elements into one of the
+-- other loops.
+local function insert_shorthand_elements(tree)
+ for i = #tree, 1, -1 do
+ local node = tree[i]
+
+ -- Insert styles
+ if node.style then
+ local props = node.style
+ if #node.style > 0 then
+ -- Make a copy of node.style without the numeric keys. This
+ -- avoids modifying node.style in case it's used for multiple
+ -- elements.
+ props = extract_props(props)
+ end
+ insert_style_elem(tree, i, node, props)
+
+ for j, substyle in ipairs(node.style) do
+ insert_style_elem(tree, i + j, node, extract_props(substyle),
+ substyle.sel:split(","))
+ end
+ end
+
+ -- Insert tooltips
+ if node.tooltip then
+ if node.name then
+ table.insert(tree, i, {
+ type = "tooltip",
+ gui_element_name = node.name,
+ tooltip_text = node.tooltip,
+ })
+ else
+ local w, h = get_and_fill_in_sizes(node)
+ table.insert(tree, i, {
+ type = "tooltip",
+ x = node.x, y = node.y, w = w, h = h,
+ tooltip_text = node.tooltip,
+ })
+ end
+ end
+
+ if node.type == "container" or node.type == "scroll_container" then
+ insert_shorthand_elements(node)
+ end
+ end
+end
-- Renders a GUI into a formspec_ast tree and a table with callbacks.
function Form:_render(player, ctx, formspec_version, id1, embedded, lang_code)
@@ -938,6 +1038,10 @@ function Form:_render(player, ctx, formspec_version, id1, embedded, lang_code)
tree, orig_form, id1, embedded
)
+ -- This should be after parse_callbacks so it can take advantage of
+ -- automatic field naming
+ insert_shorthand_elements(tree)
+
local redraw_if_changed = {}
for var in pairs(used_ctx_vars) do
-- Only add it if there is no callback and the name exists in the
diff --git a/test.lua b/test.lua
index 8710b62..cd7c1ed 100644
--- a/test.lua
+++ b/test.lua
@@ -734,4 +734,136 @@ describe("Flow", function()
end)
end)
end)
+
+ describe("inline style parser", function()
+ it("parses inline styles correctly", function()
+ test_render(gui.Box{
+ w = 1, h = 1, color = "blue",
+ style = {hello = "world"}
+ }, [[
+ size[1.6,1.6]
+ style_type[box;hello=world]
+ box[0.3,0.3;1,1;blue]
+ style_type[box;hello=]
+ ]])
+ end)
+
+ it("parses inline styles correctly", function()
+ test_render(gui.Button{
+ w = 1, h = 1, name = "mybtn",
+ style = {hello = "world"}
+ }, [[
+ size[1.6,1.6]
+ style[mybtn;hello=world]
+ button[0.3,0.3;1,1;mybtn;]
+ ]])
+ end)
+
+ it("takes advantage of auto-generated names", function()
+ test_render(gui.Button{
+ w = 1, h = 1, on_event = function() end,
+ style = {hello = "world"}
+ }, ([[
+ size[1.6,1.6]
+ style[\10;hello=world]
+ button[0.3,0.3;1,1;\10;]
+ ]]):gsub("\\1", "\1"))
+ end)
+
+ it("supports advanced selectors", function()
+ test_render(gui.Button{
+ w = 1, h = 1, name = "mybtn",
+ style = {
+ bgimg = "btn.png",
+ {sel = "$hovered", bgimg = "hover.png"},
+ {sel = "$focused", bgimg = "focus.png"},
+ },
+ }, [[
+ size[1.6,1.6]
+ style[mybtn;bgimg=btn.png]
+ style[mybtn:hovered;bgimg=hover.png]
+ style[mybtn:focused;bgimg=focus.png]
+ button[0.3,0.3;1,1;mybtn;]
+ ]])
+ end)
+
+ it("supports advanced selectors on non-named nodes", function()
+ test_render(gui.Box{
+ w = 1, h = 1, color = "blue",
+ style = {
+ bgimg = "btn.png",
+ {sel = "$hovered", bgimg = "hover.png"},
+ {sel = "$focused", bgimg = "focus.png"},
+ },
+ }, [[
+ size[1.6,1.6]
+ style_type[box;bgimg=btn.png]
+ style_type[box:hovered;bgimg=hover.png]
+ style_type[box:focused;bgimg=focus.png]
+ box[0.3,0.3;1,1;blue]
+ style_type[box:focused;bgimg=]
+ style_type[box:hovered;bgimg=]
+ style_type[box;bgimg=]
+ ]])
+ end)
+
+ it("supports multiple selectors", function()
+ test_render(gui.Button{
+ w = 1, h = 1, name = "mybtn",
+ style = {
+ bgimg = "btn.png",
+ {sel = "$hovered, $focused,$pressed", bgimg = "hover.png"},
+ },
+ }, [[
+ size[1.6,1.6]
+ style[mybtn;bgimg=btn.png]
+ style[mybtn:hovered,mybtn:focused,mybtn:pressed;bgimg=hover.png]
+ button[0.3,0.3;1,1;mybtn;]
+ ]])
+ end)
+
+ it("allows reuse of the same table", function()
+ local style = {
+ bgimg = "btn.png",
+ {sel = "$hovered", bgimg = "hover.png"},
+ }
+ test_render(gui.VBox{
+ gui.Button{w = 1, h = 1, name = "btn1", style = style},
+ gui.Button{w = 1, h = 1, name = "btn2", style = style},
+ }, [[
+ size[1.6,2.8]
+ style[btn1;bgimg=btn.png]
+ style[btn1:hovered;bgimg=hover.png]
+ button[0.3,0.3;1,1;btn1;]
+ style[btn2;bgimg=btn.png]
+ style[btn2:hovered;bgimg=hover.png]
+ button[0.3,1.5;1,1;btn2;]
+ ]])
+ end)
+ end)
+
+ describe("tooltip insertion", function()
+ it("works with named elements", function()
+ test_render(gui.Button{
+ w = 1, h = 1, name = "mybtn",
+ tooltip = "test",
+ }, [[
+ size[1.6,1.6]
+ tooltip[mybtn;test]
+ button[0.3,0.3;1,1;mybtn;]
+ ]])
+ end)
+
+ it("works with unnamed elements", function()
+ -- The tooltip[] added here takes the list spacing into account
+ test_render(gui.List{
+ w = 2, h = 2, padding = 1,
+ tooltip = "test"
+ }, [[
+ size[4.25,4.25]
+ tooltip[1,1;2.25,2.25;test]
+ list[;;1,1;2,2]
+ ]])
+ end)
+ end)
end)