From c26265e2c3dd984661e4e5f6671efeabaa7dc5bd Mon Sep 17 00:00:00 2001
From: Hugues Ross <hugues.ross@gmail.com>
Date: Sun, 7 Jun 2020 18:09:58 -0400
Subject: [PATCH] Table cleanup part 2 - use gui api

---
 formspec.lua |  79 +++++++-
 init.lua     |   2 +-
 table.lua    | 502 +++++++++++++++++++++++++++++++++++++++------------
 3 files changed, 467 insertions(+), 116 deletions(-)

diff --git a/formspec.lua b/formspec.lua
index 162f3ec..ee24b31 100644
--- a/formspec.lua
+++ b/formspec.lua
@@ -1,15 +1,32 @@
 local gui = {};
 
+function gui.formspec(args)
+    local data = string.format("formspec_version[%d] size[%f,%f]", args.version or 3, args.w, args.h);
+
+    if args.bg then
+        data = data .. gui.bg9 {
+            skin = args.bg,
+            fullsize = true,
+        };
+    end
+
+    return data;
+end
+
 function gui.bg9(args)
     return string.format("background9[%f,%f;%f,%f;%s;%s;%s]",
-                          args.x, args.y,
-                          args.w, args.h,
+                          args.x or 0, args.y or 0,
+                          args.w or 1 * (args.size or 1), args.h or 1 * (args.size or 1),
                           args.skin.texture .. ".png",
                           args.fullsize or false,
                           tostring(args.skin.radius));
 end
 
 function gui.button(args)
+    if args.disabled then
+        return string.format("button[%f,%f;%f,%f;disabled_button;%s]", args.x, args.y, args.w, args.h, args.text);
+    end
+
     local data = string.format("button[%f,%f;%f,%f;%s;%s]", args.x, args.y, args.w, args.h, args.id, args.text);
 
     if args.tooltip then
@@ -22,6 +39,19 @@ function gui.button(args)
     return data;
 end
 
+function gui.container(args)
+    local data = string.format("container[%f,%f]", args.x, args.y);
+    for _,element in ipairs(args) do
+        data = data .. element;
+    end
+
+    return data .. "container_end[]";
+end
+
+function gui.image(args)
+    return string.format("image[%f,%f;%f,%f;%s]", args.x, args.y, args.w, args.h, args.image);
+end
+
 function gui.image_button(args)
     local data = string.format("image_button[%f,%f;%f,%f;%s;%s;%s]",
                                 args.x, args.y,
@@ -40,7 +70,46 @@ function gui.image_button(args)
     return data;
 end
 
+function gui.inventory(args)
+    local data = "";
+
+    if args.bg then
+        for i = 0,args.w - 1 do
+            for j = 0,args.h - 1 do
+                data = data .. gui.bg9 {
+                    x = args.x + (i * 1.25),
+                    y = args.y + (j * 1.25),
+
+                    skin = args.bg,
+                };
+            end
+        end
+    end
+
+    data = data .. string.format("list[%s;%s;%f,%f;%f,%f;]", args.location, args.id, args.x, args.y, args.w, args.h);
+
+    if args.tooltip then
+        data = data .. gui.tooltip {
+            x = args.x,
+            y = args.y,
+            w = args.w,
+            h = args.h,
+            text = args.tooltip,
+        };
+    end
+
+    return data;
+end
+
 function gui.label(args)
+    if args.textcolor then
+        return string.format("label[%f,%f;%s%s]",
+                             args.x,
+                             args.y,
+                             minetest.get_color_escape_sequence(args.textcolor),
+                             args.text);
+    end
+
     return string.format("label[%f,%f;%s]", args.x, args.y, args.text);
 end
 
@@ -61,7 +130,11 @@ function gui.style_type(args)
 end
 
 function gui.tooltip(args)
-    return string.format("tooltip[%s;%s]", args.id, args.text);
+    if args.id then
+        return string.format("tooltip[%s;%s]", args.id, args.text);
+    else
+        return string.format("tooltip[%f,%f;%f,%f;%s]", args.x, args.y, args.w, args.h, args.text);
+    end
 end
 
 return gui;
diff --git a/init.lua b/init.lua
index c8d32e8..bdb9de4 100644
--- a/init.lua
+++ b/init.lua
@@ -58,4 +58,4 @@ loadfile(modpath .. "/items.lua") ();
 _cartographer.generate_marker_formspec = loadfile(modpath .. "/marker_formspec.lua") (_cartographer.marker_lookup, cartographer.gui);
 loadfile(modpath .. "/map_formspec.lua") (map_data);
 loadfile(modpath .. "/commands.lua") ();
-loadfile(modpath .. "/table.lua") (_cartographer.materials_by_name, _cartographer.materials_by_group, cartographer.skin);
+loadfile(modpath .. "/table.lua") (_cartographer.materials_by_name, _cartographer.materials_by_group, cartographer.gui, cartographer.skin);
diff --git a/table.lua b/table.lua
index 30aad72..7a14995 100644
--- a/table.lua
+++ b/table.lua
@@ -1,4 +1,4 @@
-local materials_by_name, materials_by_group, gui_skin = ...;
+local materials_by_name, materials_by_group, gui, gui_skin = ...;
 
 local MAP_SIZE = 40;
 local SCALE_SMALL = 1;
@@ -6,28 +6,6 @@ local SCALE_MEDIUM = 2;
 local SCALE_LARGE = 4;
 local SCALE_HUGE = 8;
 
--- Draw background elements in the same arrangement as inventory slots
--- x: The x position of the inventory
--- y: The y position of the inventory
--- cols: The width of the inventory, in columns
--- rows: The height of the inventory, in rows
--- skin: A 9-slice background skin table
---
--- Returns a formspec string
-local function inventory_bg(x, y, cols, rows, skin)
-    local data = "";
-    for i = 0,cols - 1 do
-        for j = 0,rows - 1 do
-            data = data .. string.format("background9[%f,%f;1,1;%s.png;false;%s]",
-                                        x + (i * 1.25), y + (j * 1.25),
-                                        skin.texture,
-                                        tostring(skin.radius));
-        end
-    end
-
-    return data;
-end
-
 -- Get the material cost for the given map scale and detail level
 -- scale: The map scale
 -- detail: The detail level
@@ -63,7 +41,8 @@ local function get_craft_material_cost(meta)
 
     if stack:get_name() == "cartographer:map" then
         local smeta = stack:get_meta();
-        local sub_cost = get_material_cost(smeta:get_int("cartographer:scale") or SCALE_SMALL, (smeta:get_int("cartographer:detail") or 1) - 1);
+        local sub_cost = get_material_cost(smeta:get_int("cartographer:scale") or SCALE_SMALL,
+                                          (smeta:get_int("cartographer:detail") or 1) - 1);
         is_positive = cost.paper >= sub_cost.paper and cost.pigment >= sub_cost.pigment;
         cost.paper = math.max(cost.paper - sub_cost.paper, 0);
         cost.pigment = math.max(cost.pigment - sub_cost.pigment, 0);
@@ -142,31 +121,20 @@ end
 
 local fs = {};
 
--- Draw a button, with support for enabled/disabled states
--- x: The x position of the button
--- y: The y position of the button
--- w: The width of the button
--- h: The height of the button
--- id: The element id
--- text: The text to display in the button
--- enabled: Whether or not the button is enabled
---
--- Returns a formspec string
-function fs.button(x, y, w, h, id, text, enabled)
-    if enabled then
-        return string.format("button[%f,%f;%f,%f;%s;%s]", x, y, w, h, id, text);
-    end
-
-    return string.format("button[%f,%f;%f,%f;disabled_button;%s]", x, y, w, h, text);
-end
-
 -- Draw a 1px thick horizontal separator formspec element
 -- y: The y position of the separator
 -- skin: A 9-slice background skin table
 --
 -- Returns a formspec string
 function fs.separator(y, skin)
-    return string.format("background9[0.1,%f;10.05,0.01;%s.png;false;%s]", y, skin.texture, tostring(skin.radius))
+    return gui.bg9 {
+        x = 0.1,
+        y = y,
+        w = 10.05,
+        h = 0.01,
+
+        skin = skin,
+    };
 end
 
 -- Draw all the essential formspec data (size, background, styles, tabs)
@@ -178,21 +146,106 @@ end
 --
 -- Returns a formspec string
 function fs.header(w, h, rank, tab, skin)
-    local data = "formspec_version[3]"
-              .. string.format("size[%f,%f]", w, h)
-              .. string.format("background9[-0.1,0;1,1;%s.png;true;%s]", skin.background.texture, tostring(skin.background.radius))
-              .. string.format("background9[0.0625,0.125;%f,%f;%s.png;false;%s]", w - 0.125, h - 0.25, skin.inner_background.texture, tostring(skin.inner_background.radius))
-              .. string.format("style_type[button;noclip=true;border=false;bgimg=%s.png;bgimg_hovered=%s.png;bgimg_pressed=%s.png;bgimg_middle=%s;textcolor=%s]", skin.tab.texture, skin.tab.hovered_texture, skin.tab.pressed_texture, tostring(skin.tab.radius), skin.tab.font_color)
-              .. string.format("style[tab%d;noclip=true;border=false;bgimg=%s.png;bgimg_hovered=%s.png;bgimg_pressed=%s.png;bgimg_middle=%s;textcolor=%s]", tab, skin.tab.selected_texture, skin.tab.selected_texture, skin.tab.selected_texture, tostring(skin.tab.radius), skin.tab.font_color)
-              .. string.format("button[0.25,-0.425;1.5,0.55;tab1;Materials]", tab)
-              .. string.format("button[1.75,-0.425;1.5,0.55;tab2;Create Map]", tab);
+    local data = {
+        gui.formspec {
+            w = w,
+            h = h,
+
+            bg = skin.background,
+        },
+        gui.bg9 {
+            x = 0.0625,
+            y = 0.125,
+
+            w = w - 0.125,
+            h = h - 0.25,
+
+            skin = skin.inner_background,
+        },
+
+        gui.style_type {
+            selector = "button",
+            properties = {
+                noclip = true,
+                border = false,
+
+                bgimg = skin.tab.texture .. ".png",
+                bgimg_hovered = skin.tab.hovered_texture .. ".png",
+                bgimg_pressed = skin.tab.pressed_texture .. ".png",
+                bgimg_middle = skin.tab.radius,
+                textcolor = skin.tab.font_color,
+            }
+        },
+        gui.style {
+            selector = "tab" .. tostring(tab),
+            properties = {
+                bgimg = skin.tab.selected_texture .. ".png",
+                bgimg_hovered = skin.tab.selected_texture .. ".png",
+                bgimg_pressed = skin.tab.selected_texture .. ".png",
+            }
+        },
+
+        gui.button {
+            x = 0.25,
+            y = -0.425,
+
+            w = 1.5,
+            h = 0.55,
+
+            id = "tab1",
+
+            text = "Materials"
+        },
+        gui.button {
+            x = 1.75,
+            y = -0.425,
+
+            w = 1.5,
+            h = 0.55,
+
+            id = "tab2",
+
+            text = "Create Map"
+        },
+    };
 
     if rank >= 2 then
-        data = data .. string.format("button[3.25,-0.425;1.5,0.55;tab3;Copy Map]", tab);
+        table.insert(data, gui.button {
+            x = 3.25,
+            y = -0.425,
+
+            w = 1.5,
+            h = 0.55,
+
+            id = "tab3",
+
+            text = "Copy Map"
+        });
     end
 
-    return data .. string.format("style_type[button;border=false;bgimg=%s.png;bgimg_hovered=%s.png;bgimg_pressed=%s.png;bgimg_middle=%s;textcolor=%s]", skin.button.texture, skin.button.hovered_texture, skin.button.pressed_texture, tostring(skin.button.radius), skin.button.font_color)
-                .. string.format("style[disabled_button;bgimg=;bgimg_hovered=;bgimg_pressed=;textcolor=%s]", skin.button.disabled_font_color);
+    table.insert(data, gui.style_type {
+        selector = "button",
+        properties = {
+            bgimg = skin.button.texture .. ".png",
+            bgimg_hovered = skin.button.hovered_texture .. ".png",
+            bgimg_pressed = skin.button.pressed_texture .. ".png",
+            bgimg_middle = skin.button.radius,
+
+            textcolor = skin.button.font_color,
+        },
+    });
+    table.insert(data, gui.style {
+        selector = "disabled_button",
+        properties = {
+            bgimg = "",
+            bgimg_hovered = "",
+            bgimg_pressed = "",
+
+            textcolor = skin.button.disabled_font_color,
+        },
+    });
+
+    return table.concat(data);
 end
 
 -- Draw material counters from a table's metadata
@@ -203,41 +256,116 @@ end
 --
 -- Returns a formspec string
 function fs.materials(x, y, meta, skin)
-    return string.format("container[%f,%f]", x, y)
-        .. "formspec_version[3]"
-        .. string.format("background9[0,0.125;1,0.25;%s.png;false;%s]", skin.label.texture, tostring(skin.label.radius))
-        .. string.format("image[0.125,0.125;0.25,0.25;%s.png]", skin.paper_texture)
-        .. string.format("label[0.375,0.25;%sx %d]", minetest.get_color_escape_sequence(skin.label.font_color), meta:get_int("paper"))
-        .. string.format("background9[1.25,0.125;1,0.25;%s.png;false;%s]", skin.label.texture, tostring(skin.label.radius))
-        .. string.format("image[1.375,0.125;0.25,0.25;%s.png]", skin.pigment_texture)
-        .. string.format("label[1.625,0.25;%sx %d]", minetest.get_color_escape_sequence(skin.label.font_color), meta:get_int("pigment"))
-        .. "container_end[]";
+    return gui.container {
+        x = x,
+        y = y,
+
+        gui.bg9 {
+            x = 0,
+            y = 0.125,
+
+            w = 1.0,
+            h = 0.25,
+
+            skin = skin.label,
+        },
+        gui.image {
+            x = 0.125,
+            y = 0.125,
+
+            w = 0.25,
+            h = 0.25,
+
+            image = skin.paper_texture .. ".png",
+        },
+        gui.label {
+            x = 0.375,
+            y = 0.25,
+
+            textcolor = skin.label.font_color,
+            text = string.format("x %d", meta:get_int("paper")),
+        },
+
+        gui.bg9 {
+            x = 1.25,
+            y = 0.125,
+
+            w = 1.0,
+            h = 0.25,
+
+            skin = skin.label,
+        },
+        gui.image {
+            x = 1.375,
+            y = 0.125,
+
+            w = 0.25,
+            h = 0.25,
+
+            image = skin.pigment_texture .. ".png",
+        },
+        gui.label {
+            x = 1.625,
+            y = 0.25,
+
+            textcolor = skin.label.font_color,
+            text = string.format("x %d", meta:get_int("pigment")),
+        },
+    };
 end
 
 -- Draw a label with material costs from a table
 -- x: The x position of the interface
 -- y: The y position of the interface
 -- cost: A table of material costs, with string keys for the material
---       names and iteger values
+--       names and integer values
 -- skin: A formspec skin table
 --
 -- Returns a formspec string
 function fs.cost(x, y, cost, skin)
-    local data = string.format("background9[%f,%f;1,0.5;%s.png;false;%s]", x, y - 0.125, skin.label.texture, tostring(skin.label.radius));
-    local i = 0;
+    local data = {
+        gui.bg9 {
+            x = x,
+            y = y - 0.125,
+            w = 1,
+            h = 0.5,
 
+            skin = skin.label,
+        },
+    }
+
+    local i = 0;
     for name,value in pairs(cost) do
+        local texture = "";
         if name == "paper" then
-            data = data .. string.format("image[%f,%f;0.25,0.25;%s.png]", x + 0.125, y + (i * 0.25) - 0.125, skin.paper_texture)
+            texture = skin.paper_texture .. ".png";
         elseif name == "pigment" then
-            data = data .. string.format("image[%f,%f;0.25,0.25;%s.png]", x + 0.125, y + (i * 0.25) - 0.125, skin.pigment_texture)
+            texture = skin.pigment_texture .. ".png";
         end
 
-        data = data .. string.format("label[%f,%f;%sx %d]", x + 0.375, y + (i * 0.25), minetest.get_color_escape_sequence(skin.label.font_color), value);
+        table.insert(data, gui.image {
+            x = x + 0.125,
+            y = y + (i * 0.25) - 0.125,
+            w = 0.25,
+            h = 0.25,
+
+            image = texture,
+        });
+
+        table.insert(data, gui.label {
+            x = x + 0.375,
+            y = y + (i * 0.25);
+            w = 0.25,
+            h = 0.25,
+
+            textcolor = skin.label.font_color,
+            text = string.format("x %d", value);
+        });
+
         i = i + 1;
     end
 
-    return data;
+    return table.concat(data);
 end
 
 -- Draw the material conversion tab UI
@@ -251,14 +379,35 @@ function fs.convert(x, y, pos, skin)
     local meta = minetest.get_meta(pos);
     local value = cartographer.get_material_value(meta:get_inventory():get_stack("input", 1));
 
-    return string.format("container[%f,%f]", x, y)
-        .. "formspec_version[3]"
-        .. inventory_bg(0, 0, 1, 1, skin.slot)
-        .. string.format("list[nodemeta:%d,%d,%d;input;0,0;1,1;]", pos.x, pos.y, pos.z)
-        .. "tooltip[0,0;1,1;Place items here to convert\nthem into mapmaking materials]"
-        .. fs.button(2.5, 0.25, 2, 0.5, "convert", "Convert Materials", value.paper + value.pigment > 0)
-        .. fs.cost(1.25, 0.375, value, skin)
-        .. "container_end[]";
+    return gui.container {
+        x = x,
+        y = y,
+
+        gui.inventory {
+            x = 0,
+            y = 0,
+            w = 1,
+            h = 1,
+
+            location = string.format("nodemeta:%d,%d,%d", pos.x, pos.y, pos.z),
+            id = "input",
+            bg = skin.slot,
+            tooltip = "Place items here to convert\nthem into mapmaking materials",
+        },
+
+        gui.button {
+            x = 2.5,
+            y = 0.25,
+            w = 2,
+            h = 0.5,
+
+            id = "convert",
+            text = "Convert Materials",
+            disabled = value.paper + value.pigment <= 0,
+        },
+
+        fs.cost(1.25, 0.375, value, skin),
+    };
 end
 
 -- Draw the map crafting tab UI
@@ -272,38 +421,135 @@ end
 -- Returns a formspec string
 function fs.craft(x, y, pos, rank, meta, skin)
     local cost, is_positive = get_craft_material_cost(meta);
-    local data = string.format("container[%f,%f]", x, y)
-              .. "formspec_version[3]"
-              .. inventory_bg(0, 1, 1, 1, skin.slot)
-              .. string.format("list[nodemeta:%d,%d,%d;output;0,1;1,1;]", pos.x, pos.y, pos.z)
-              .. "tooltip[0,1;1,1;Place a map here to upgrade it,\nor leave empty to craft]"
-              .. fs.button(2.5, 1.25, 2, 0.5, "craft", "Craft Map", is_positive and can_afford(cost, meta))
-              .. fs.cost(1.25, 1.375, cost, skin);
+
+    local data = {
+        x = x,
+        y = y,
+
+        gui.inventory {
+            x = 0,
+            y = 1,
+            w = 1,
+            h = 1,
+
+            location = string.format("nodemeta:%d,%d,%d", pos.x, pos.y, pos.z),
+            id = "output",
+            bg = skin.slot,
+            tooltip = "Place a map here to upgrade it,\nor leave empty to craft",
+        },
+        gui.button {
+            x = 2.5,
+            y = 1.25,
+            w = 2,
+            h = 0.5,
+
+            id = "craft",
+            text = "Craft Map",
+            disabled = not (is_positive and can_afford(cost, meta)),
+        },
+        fs.cost(1.25, 1.375, cost, skin),
+
+        gui.style {
+            selector = string.format("%dx,%d", meta:get_int("scale"), meta:get_int("detail") + 1),
+            properties = {
+                bgimg = skin.button.selected_texture .. ".png",
+                bgimg_hovered = skin.button.selected_texture .. ".png",
+                bgimg_pressed = skin.button.selected_texture .. ".png",
+            },
+        },
+        gui.label {
+            x = 0,
+            y = 0,
+
+            text = "Detail Level",
+            textcolor = skin.label.font_color,
+        },
+    };
 
     if rank > 1 then
-        data = data .. string.format("style[%dx;bgimg=%s.png;bgimg_hovered=%s.png;bgimg_pressed=%s.png]", meta:get_int("scale"), skin.button.selected_texture, skin.button.selected_texture, skin.button.selected_texture)
-                    .. string.format("label[2.5,0;%sMap Scale]", minetest.get_color_escape_sequence(skin.label.font_color))
-                    .. "button[2.5,0.25;0.5,0.5;1x;1x]"
-                    .. "button[3.0,0.25;0.5,0.5;2x;2x]";
+        table.insert(data, gui.button {
+            x = 2.5,
+            y = 0.25,
+            w = 0.5,
+            h = 0.5,
+
+            id = "1x",
+            text = "1x",
+        });
+        table.insert(data, gui.button {
+            x = 3.0,
+            y = 0.25,
+            w = 0.5,
+            h = 0.5,
+
+            id = "2x",
+            text = "2x",
+        });
 
         if rank > 2 then
-            data = data .. "button[3.5,0.25;0.5,0.5;4x;4x]"
-                        .. "button[4.0,0.25;0.5,0.5;8x;8x]";
+            table.insert(data, gui.button {
+                x = 3.5,
+                y = 0.25,
+                w = 0.5,
+                h = 0.5,
+
+                id = "4x",
+                text = "4x",
+            });
+            table.insert(data, gui.button {
+                x = 4.0,
+                y = 0.25,
+                w = 0.5,
+                h = 0.5,
+
+                id = "8x",
+                text = "8x",
+            });
         end
     end
 
-    data = data .. string.format("style[%d;bgimg=%s.png;bgimg_hovered=%s.png;bgimg_pressed=%s.png]", meta:get_int("detail") + 1, skin.button.selected_texture, skin.button.selected_texture, skin.button.selected_texture)
-                .. string.format("label[0,0;%sDetail Level]", minetest.get_color_escape_sequence(skin.label.font_color))
-                .. "button[0.0,0.25;0.5,0.5;1;1]"
-                .. "button[0.5,0.25;0.5,0.5;2;2]";
+    table.insert(data, gui.button {
+        x = 0,
+        y = 0.25,
+        w = 0.5,
+        h = 0.5,
+
+        id = "1",
+        text = "1",
+    });
+    table.insert(data, gui.button {
+        x = 0.5,
+        y = 0.25,
+        w = 0.5,
+        h = 0.5,
+
+        id = "2",
+        text = "2",
+    });
     if rank > 1 then
-        data = data .. "button[1.0,0.25;0.5,0.5;3;3]";
+        table.insert(data, gui.button {
+            x = 1.0,
+            y = 0.25,
+            w = 0.5,
+            h = 0.5,
+
+            id = "3",
+            text = "3",
+        });
         if rank > 2 then
-            data = data .. "button[1.5,0.25;0.5,0.5;4;4]";
+            table.insert(data, gui.button {
+                x = 1.5,
+                y = 0.25,
+                w = 0.5,
+                h = 0.5,
+
+                id = "4",
+                text = "4",
+            });
         end
     end
 
-    return data .. "container_end[]";
+    return gui.container(data);
 end
 
 -- Draw the map copying tab UI
@@ -317,15 +563,42 @@ function fs.copy(x, y, pos, skin)
     local meta = minetest.get_meta(pos);
     local costs = get_copy_material_cost(meta);
 
-    return string.format("container[%f,%f]", x, y)
-        .. "formspec_version[3]"
-        .. inventory_bg(0, 0, 1, 1, skin.slot)
-        .. string.format("list[nodemeta:%d,%d,%d;copy_input;0,0;1,1;]", pos.x, pos.y, pos.z)
-        .. fs.button(2.5, 0.25, 2, 0.5, "copy", "Copy Map", can_afford(costs, meta))
-        .. fs.cost(1.25, 0.375, costs, skin)
-        .. inventory_bg(8.75, 0, 1, 1, skin.slot)
-        .. string.format("list[nodemeta:%d,%d,%d;copy_output;8.75,0;1,1;]", pos.x, pos.y, pos.z)
-        .. "container_end[]";
+    return gui.container {
+        x = x,
+        y = y,
+
+        gui.inventory {
+            x = 0,
+            y = 0,
+            w = 1,
+            h = 1,
+
+            location = string.format("nodemeta:%d,%d,%d", pos.x, pos.y, pos.z),
+            id = "copy_input",
+            bg = skin.slot,
+        },
+        gui.inventory {
+            x = 8.75,
+            y = 0,
+            w = 1,
+            h = 1,
+
+            location = string.format("nodemeta:%d,%d,%d", pos.x, pos.y, pos.z),
+            id = "copy_output",
+            bg = skin.slot,
+        },
+        gui.button {
+            x = 2.5,
+            y = 0.25,
+            w = 2,
+            h = 0.5,
+
+            id = "copy",
+            text = "Copy Map",
+            disabled = not can_afford(costs, meta),
+        },
+        fs.cost(1.25, 0.375, costs, skin),
+    };
 end
 
 -- Draw the player's inventory
@@ -335,11 +608,16 @@ end
 --
 -- Returns a formspec string
 function fs.inv(x, y, skin)
-    return string.format("container[%f,%f]", x, y)
-        .. "formspec_version[3]"
-        .. inventory_bg(0, 0, 8, 4, skin.slot)
-        .. "list[current_player;main;0,0;8,4;]"
-        .. "container_end[]";
+    return gui.inventory {
+        x = x,
+        y = y,
+        w = 8,
+        h = 4,
+
+        location = "current_player",
+        id = "main",
+        bg = skin.slot,
+    };
 end
 
 local player_tables = {};