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)