Add inline styles and tooltips

This commit is contained in:
luk3yx 2024-05-15 10:49:36 +12:00
parent 1d6ddb368d
commit 5215229d8c
3 changed files with 346 additions and 41 deletions

151
README.md
View File

@ -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
<details>
<summary><b>Tooltips</b></summary>
You can add tooltips to elements using the `tooltip` field:
```lua
gui.Image{
w = 2, h = 2,
texture_name = "air.png",
tooltip = "Air",
}
```
</details><details>
<summary><b>Hiding elements</b></summary>
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
</details><details>
<summary><b>Using a form as an inventory</b></summary>
> [!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.
</details>
### 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).
</details><details>
<summary><b>Using a form as an inventory</b></summary>
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.
</details><details>
<summary><b>Rendering to a formspec</b></summary>
@ -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!
</details>

104
init.lua
View File

@ -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

132
test.lua
View File

@ -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)