]> git.lizzy.rs Git - dragonfireclient.git/blobdiff - src/gui/guiFormSpecMenu.cpp
Formspecs: Introduce formspec_version to mods
[dragonfireclient.git] / src / gui / guiFormSpecMenu.cpp
index dfb167ce7f518b905fe1d984b8438de25a1e7c2f..f291b4e877b09747bf4d9632ba4c958638799274 100644 (file)
@@ -23,11 +23,12 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #include <iterator>
 #include <sstream>
 #include <limits>
+#include "guiButton.h"
 #include "guiFormSpecMenu.h"
 #include "guiTable.h"
 #include "constants.h"
 #include "gamedef.h"
-#include "keycode.h"
+#include "client/keycode.h"
 #include "util/strfnd.h"
 #include <IGUICheckBox.h>
 #include <IGUIEditBox.h>
@@ -47,13 +48,13 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #include "mainmenumanager.h"
 #include "porting.h"
 #include "settings.h"
-#include "client.h"
-#include "fontengine.h"
+#include "client/client.h"
+#include "client/fontengine.h"
 #include "util/hex.h"
 #include "util/numeric.h"
 #include "util/string.h" // for parseColorString()
 #include "irrlicht_changes/static_text.h"
-#include "guiscalingfilter.h"
+#include "client/guiscalingfilter.h"
 #include "guiEditBoxWithScrollbar.h"
 #include "intlGUIEditBox.h"
 
@@ -66,8 +67,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 
 #define MY_CHECKGEOM(a,b)                                                                                                      \
        if (v_geom.size() != 2) {                                                                                               \
-               errorstream<< "Invalid pos for element " << a << "specified: \""        \
-                       << parts[b] << "\"" << std::endl;                                                               \
+               errorstream<< "Invalid geometry for element " << a <<                           \
+                       "specified: \"" << parts[b] << "\"" << std::endl;                               \
                        return;                                                                                                                 \
        }
 /*
@@ -86,18 +87,17 @@ inline u32 clamp_u8(s32 value)
 GUIFormSpecMenu::GUIFormSpecMenu(JoystickController *joystick,
                gui::IGUIElement *parent, s32 id, IMenuManager *menumgr,
                Client *client, ISimpleTextureSource *tsrc, IFormSource *fsrc, TextDest *tdst,
-               bool remap_dbl_click) :
+               const std::string &formspecPrepend,
+               bool remap_dbl_click):
        GUIModalMenu(RenderingEngine::get_gui_env(), parent, id, menumgr),
        m_invmgr(client),
        m_tsrc(tsrc),
        m_client(client),
+       m_formspec_prepend(formspecPrepend),
        m_form_src(fsrc),
        m_text_dst(tdst),
        m_joystick(joystick),
        m_remap_dbl_click(remap_dbl_click)
-#ifdef __ANDROID__
-       , m_JavaDialogFieldName("")
-#endif
 {
        current_keys_pending.key_down = false;
        current_keys_pending.key_up = false;
@@ -128,11 +128,12 @@ GUIFormSpecMenu::~GUIFormSpecMenu()
 }
 
 void GUIFormSpecMenu::create(GUIFormSpecMenu *&cur_formspec, Client *client,
-       JoystickController *joystick, IFormSource *fs_src, TextDest *txt_dest)
+       JoystickController *joystick, IFormSource *fs_src, TextDest *txt_dest,
+       const std::string &formspecPrepend)
 {
        if (cur_formspec == nullptr) {
                cur_formspec = new GUIFormSpecMenu(joystick, guiroot, -1, &g_menumgr,
-                       client, client->getTextureSource(), fs_src, txt_dest);
+                       client, client->getTextureSource(), fs_src, txt_dest, formspecPrepend);
                cur_formspec->doPause = false;
 
                /*
@@ -144,6 +145,7 @@ void GUIFormSpecMenu::create(GUIFormSpecMenu *&cur_formspec, Client *client,
                */
 
        } else {
+               cur_formspec->setFormspecPrepend(formspecPrepend);
                cur_formspec->setFormSource(fs_src);
                cur_formspec->setTextDest(txt_dest);
        }
@@ -254,6 +256,40 @@ std::vector<std::string>* GUIFormSpecMenu::getDropDownValues(const std::string &
        return NULL;
 }
 
+v2s32 GUIFormSpecMenu::getElementBasePos(bool absolute,
+               const std::vector<std::string> *v_pos)
+{
+       v2s32 pos = padding;
+       if (absolute)
+               pos += AbsoluteRect.UpperLeftCorner;
+
+       v2f32 pos_f = v2f32(pos.X, pos.Y) + pos_offset * spacing;
+       if (v_pos) {
+               pos_f.X += stof((*v_pos)[0]) * spacing.X;
+               pos_f.Y += stof((*v_pos)[1]) * spacing.Y;
+       }
+       return v2s32(pos_f.X, pos_f.Y);
+}
+
+v2s32 GUIFormSpecMenu::getRealCoordinateBasePos(bool absolute,
+               const std::vector<std::string> &v_pos)
+{
+       v2f32 pos_f = v2f32(0.0f, 0.0f);
+
+       pos_f.X += stof(v_pos[0]) + pos_offset.X;
+       pos_f.Y += stof(v_pos[1]) + pos_offset.Y;
+
+       if (absolute)
+               return v2s32(pos_f.X * imgsize.X + AbsoluteRect.UpperLeftCorner.X,
+                               pos_f.Y * imgsize.Y + AbsoluteRect.UpperLeftCorner.Y);
+       return v2s32(pos_f.X * imgsize.X, pos_f.Y * imgsize.Y);
+}
+
+v2s32 GUIFormSpecMenu::getRealCoordinateGeometry(const std::vector<std::string> &v_geom)
+{
+       return v2s32(stof(v_geom[0]) * imgsize.X, stof(v_geom[1]) * imgsize.Y);
+}
+
 void GUIFormSpecMenu::parseSize(parserData* data, const std::string &element)
 {
        std::vector<std::string> parts = split(element,',');
@@ -268,12 +304,13 @@ void GUIFormSpecMenu::parseSize(parserData* data, const std::string &element)
                data->invsize.Y = MYMAX(0, stof(parts[1]));
 
                lockSize(false);
+#ifndef __ANDROID__
                if (parts.size() == 3) {
                        if (parts[2] == "true") {
                                lockSize(true,v2u32(800,600));
                        }
                }
-
+#endif
                data->explicit_size = true;
                return;
        }
@@ -289,8 +326,8 @@ void GUIFormSpecMenu::parseContainer(parserData* data, const std::string &elemen
                        parts[1] = parts[1].substr(0, parts[1].find(';'));
 
                container_stack.push(pos_offset);
-               pos_offset.X += MYMAX(0, stof(parts[0]));
-               pos_offset.Y += MYMAX(0, stof(parts[1]));
+               pos_offset.X += stof(parts[0]);
+               pos_offset.Y += stof(parts[1]);
                return;
        }
        errorstream<< "Invalid container start element (" << parts.size() << "): '" << element << "'"  << std::endl;
@@ -331,16 +368,19 @@ void GUIFormSpecMenu::parseList(parserData* data, const std::string &element)
 
                InventoryLocation loc;
 
-               if(location == "context" || location == "current_name")
+               if (location == "context" || location == "current_name")
                        loc = m_current_inventory_location;
                else
                        loc.deSerialize(location);
 
-               v2s32 pos = padding + AbsoluteRect.UpperLeftCorner + pos_offset * spacing;
-               pos.X += stof(v_pos[0]) * (float)spacing.X;
-               pos.Y += stof(v_pos[1]) * (float)spacing.Y;
-
+               v2s32 pos;
                v2s32 geom;
+
+               if (data->real_coordinates)
+                       pos = getRealCoordinateBasePos(true, v_pos);
+               else
+                       pos = getElementBasePos(true, &v_pos);
+
                geom.X = stoi(v_geom[0]);
                geom.Y = stoi(v_geom[1]);
 
@@ -355,7 +395,7 @@ void GUIFormSpecMenu::parseList(parserData* data, const std::string &element)
 
                if(!data->explicit_size)
                        warningstream<<"invalid use of list without a size[] element"<<std::endl;
-               m_inventorylists.emplace_back(loc, listname, pos, geom, start_i);
+               m_inventorylists.emplace_back(loc, listname, pos, geom, start_i, data->real_coordinates);
                return;
        }
        errorstream<< "Invalid list element(" << parts.size() << "): '" << element << "'"  << std::endl;
@@ -416,21 +456,37 @@ void GUIFormSpecMenu::parseCheckbox(parserData* data, const std::string &element
 
                MY_CHECKPOS("checkbox",0);
 
-               v2s32 pos = padding + pos_offset * spacing;
-               pos.X += stof(v_pos[0]) * (float) spacing.X;
-               pos.Y += stof(v_pos[1]) * (float) spacing.Y;
-
                bool fselected = false;
 
                if (selected == "true")
                        fselected = true;
 
                std::wstring wlabel = translate_string(utf8_to_wide(unescape_string(label)));
-
-               core::rect<s32> rect = core::rect<s32>(
-                               pos.X, pos.Y + ((imgsize.Y/2) - m_btn_height),
-                               pos.X + m_font->getDimension(wlabel.c_str()).Width + 25, // text size + size of checkbox
-                               pos.Y + ((imgsize.Y/2) + m_btn_height));
+               const core::dimension2d<u32> label_size = m_font->getDimension(wlabel.c_str());
+               s32 cb_size = Environment->getSkin()->getSize(gui::EGDS_CHECK_BOX_WIDTH);
+               s32 y_center = (std::max(label_size.Height, (u32)cb_size) + 1) / 2;
+
+               v2s32 pos;
+               core::rect<s32> rect;
+
+               if (data->real_coordinates) {
+                       pos = getRealCoordinateBasePos(false, v_pos);
+
+                       rect = core::rect<s32>(
+                                       pos.X,
+                                       pos.Y - y_center,
+                                       pos.X + label_size.Width + cb_size + 7,
+                                       pos.Y + y_center
+                               );
+               } else {
+                       pos = getElementBasePos(false, &v_pos);
+                       rect = core::rect<s32>(
+                                       pos.X,
+                                       pos.Y + imgsize.Y / 2 - y_center,
+                                       pos.X + label_size.Width + cb_size + 7,
+                                       pos.Y + imgsize.Y / 2 + y_center
+                               );
+               }
 
                FieldSpec spec(
                                name,
@@ -444,6 +500,9 @@ void GUIFormSpecMenu::parseCheckbox(parserData* data, const std::string &element
                gui::IGUICheckBox* e = Environment->addCheckBox(fselected, rect, this,
                                        spec.fid, spec.flabel.c_str());
 
+               auto style = getStyleForElement("checkbox", name);
+               e->setNotClipped(style.getBool(StyleSpec::NOCLIP, false));
+
                if (spec.fname == data->focused_fieldname) {
                        Environment->setFocus(e);
                }
@@ -461,26 +520,25 @@ void GUIFormSpecMenu::parseScrollBar(parserData* data, const std::string &elemen
 
        if (parts.size() >= 5) {
                std::vector<std::string> v_pos = split(parts[0],',');
-               std::vector<std::string> v_dim = split(parts[1],',');
+               std::vector<std::string> v_geom = split(parts[1],',');
                std::string name = parts[3];
                std::string value = parts[4];
 
                MY_CHECKPOS("scrollbar",0);
+               MY_CHECKGEOM("scrollbar",1);
 
-               v2s32 pos = padding + pos_offset * spacing;
-               pos.X += stof(v_pos[0]) * (float) spacing.X;
-               pos.Y += stof(v_pos[1]) * (float) spacing.Y;
+               v2s32 pos;
+               v2s32 dim;
 
-               if (v_dim.size() != 2) {
-                       errorstream<< "Invalid size for element " << "scrollbar"
-                               << "specified: \"" << parts[1] << "\"" << std::endl;
-                       return;
+               if (data->real_coordinates) {
+                       pos = getRealCoordinateBasePos(false, v_pos);
+                       dim = getRealCoordinateGeometry(v_geom);
+               } else {
+                       pos = getElementBasePos(false, &v_pos);
+                       dim.X = stof(v_geom[0]) * spacing.X;
+                       dim.Y = stof(v_geom[1]) * spacing.Y;
                }
 
-               v2s32 dim;
-               dim.X = stof(v_dim[0]) * (float) spacing.X;
-               dim.Y = stof(v_dim[1]) * (float) spacing.Y;
-
                core::rect<s32> rect =
                                core::rect<s32>(pos.X, pos.Y, pos.X + dim.X, pos.Y + dim.Y);
 
@@ -501,6 +559,9 @@ void GUIFormSpecMenu::parseScrollBar(parserData* data, const std::string &elemen
                gui::IGUIScrollBar* e =
                                Environment->addScrollBar(is_horizontal,rect,this,spec.fid);
 
+               auto style = getStyleForElement("scrollbar", name);
+               e->setNotClipped(style.getBool(StyleSpec::NOCLIP, false));
+
                e->setMax(1000);
                e->setMin(0);
                e->setPos(stoi(parts[4]));
@@ -528,13 +589,17 @@ void GUIFormSpecMenu::parseImage(parserData* data, const std::string &element)
                MY_CHECKPOS("image", 0);
                MY_CHECKGEOM("image", 1);
 
-               v2s32 pos = padding + AbsoluteRect.UpperLeftCorner + pos_offset * spacing;
-               pos.X += stof(v_pos[0]) * (float) spacing.X;
-               pos.Y += stof(v_pos[1]) * (float) spacing.Y;
-
+               v2s32 pos;
                v2s32 geom;
-               geom.X = stof(v_geom[0]) * (float)imgsize.X;
-               geom.Y = stof(v_geom[1]) * (float)imgsize.Y;
+
+               if (data->real_coordinates) {
+                       pos = getRealCoordinateBasePos(true, v_pos);
+                       geom = getRealCoordinateGeometry(v_geom);
+               } else {
+                       pos = getElementBasePos(true, &v_pos);
+                       geom.X = stof(v_geom[0]) * (float)imgsize.X;
+                       geom.Y = stof(v_geom[1]) * (float)imgsize.Y;
+               }
 
                if (!data->explicit_size)
                        warningstream<<"invalid use of image without a size[] element"<<std::endl;
@@ -548,9 +613,7 @@ void GUIFormSpecMenu::parseImage(parserData* data, const std::string &element)
 
                MY_CHECKPOS("image", 0);
 
-               v2s32 pos = padding + AbsoluteRect.UpperLeftCorner + pos_offset * spacing;
-               pos.X += stof(v_pos[0]) * (float) spacing.X;
-               pos.Y += stof(v_pos[1]) * (float) spacing.Y;
+               v2s32 pos = getElementBasePos(true, &v_pos);
 
                if (!data->explicit_size)
                        warningstream<<"invalid use of image without a size[] element"<<std::endl;
@@ -574,13 +637,17 @@ void GUIFormSpecMenu::parseItemImage(parserData* data, const std::string &elemen
                MY_CHECKPOS("itemimage",0);
                MY_CHECKGEOM("itemimage",1);
 
-               v2s32 pos = padding + AbsoluteRect.UpperLeftCorner + pos_offset * spacing;
-               pos.X += stof(v_pos[0]) * (float) spacing.X;
-               pos.Y += stof(v_pos[1]) * (float) spacing.Y;
-
+               v2s32 pos;
                v2s32 geom;
-               geom.X = stof(v_geom[0]) * (float)imgsize.X;
-               geom.Y = stof(v_geom[1]) * (float)imgsize.Y;
+
+               if (data->real_coordinates) {
+                       pos = getRealCoordinateBasePos(true, v_pos);
+                       geom = getRealCoordinateGeometry(v_geom);
+               } else {
+                       pos = getElementBasePos(true, &v_pos);
+                       geom.X = stof(v_geom[0]) * (float)imgsize.X;
+                       geom.Y = stof(v_geom[1]) * (float)imgsize.Y;
+               }
 
                if(!data->explicit_size)
                        warningstream<<"invalid use of item_image without a size[] element"<<std::endl;
@@ -606,17 +673,23 @@ void GUIFormSpecMenu::parseButton(parserData* data, const std::string &element,
                MY_CHECKPOS("button",0);
                MY_CHECKGEOM("button",1);
 
-               v2s32 pos = padding + pos_offset * spacing;
-               pos.X += stof(v_pos[0]) * (float)spacing.X;
-               pos.Y += stof(v_pos[1]) * (float)spacing.Y;
-
+               v2s32 pos;
                v2s32 geom;
-               geom.X = (stof(v_geom[0]) * (float)spacing.X)-(spacing.X-imgsize.X);
-               pos.Y += (stof(v_geom[1]) * (float)imgsize.Y)/2;
+               core::rect<s32> rect;
 
-               core::rect<s32> rect =
-                               core::rect<s32>(pos.X, pos.Y - m_btn_height,
+               if (data->real_coordinates) {
+                       pos = getRealCoordinateBasePos(false, v_pos);
+                       geom = getRealCoordinateGeometry(v_geom);
+                       rect = core::rect<s32>(pos.X, pos.Y, pos.X+geom.X,
+                               pos.Y+geom.Y);
+               } else {
+                       pos = getElementBasePos(false, &v_pos);
+                       geom.X = (stof(v_geom[0]) * spacing.X) - (spacing.X - imgsize.X);
+                       pos.Y += (stof(v_geom[1]) * (float)imgsize.Y)/2;
+
+                       rect = core::rect<s32>(pos.X, pos.Y - m_btn_height,
                                                pos.X + geom.X, pos.Y + m_btn_height);
+               }
 
                if(!data->explicit_size)
                        warningstream<<"invalid use of button without a size[] element"<<std::endl;
@@ -632,8 +705,38 @@ void GUIFormSpecMenu::parseButton(parserData* data, const std::string &element,
                spec.ftype = f_Button;
                if(type == "button_exit")
                        spec.is_exit = true;
-               gui::IGUIButton* e = Environment->addButton(rect, this, spec.fid,
-                               spec.flabel.c_str());
+
+               GUIButton *e = GUIButton::addButton(Environment, rect, this, spec.fid, spec.flabel.c_str());
+
+               auto style = getStyleForElement(type, name, (type != "button") ? "button" : "");
+               if (style.isNotDefault(StyleSpec::BGCOLOR)) {
+                       e->setColor(style.getColor(StyleSpec::BGCOLOR));
+               }
+               if (style.isNotDefault(StyleSpec::TEXTCOLOR)) {
+                       e->setOverrideColor(style.getColor(StyleSpec::TEXTCOLOR));
+               }
+               e->setNotClipped(style.getBool(StyleSpec::NOCLIP, false));
+               e->setDrawBorder(style.getBool(StyleSpec::BORDER, true));
+
+               if (style.isNotDefault(StyleSpec::BGIMG)) {
+                       std::string image_name = style.get(StyleSpec::BGIMG, "");
+                       std::string pressed_image_name = style.get(StyleSpec::BGIMG_PRESSED, "");
+
+                       video::ITexture *texture = 0;
+                       video::ITexture *pressed_texture = 0;
+                       texture = m_tsrc->getTexture(image_name);
+                       if (!pressed_image_name.empty())
+                               pressed_texture = m_tsrc->getTexture(pressed_image_name);
+                       else
+                               pressed_texture = texture;
+
+                       e->setUseAlphaChannel(style.getBool(StyleSpec::ALPHA, true));
+                       e->setImage(guiScalingImageButton(
+                                       Environment->getVideoDriver(), texture, geom.X, geom.Y));
+                       e->setPressedImage(guiScalingImageButton(
+                                       Environment->getVideoDriver(), pressed_texture, geom.X, geom.Y));
+                       e->setScaleImage(true);
+               }
 
                if (spec.fname == data->focused_fieldname) {
                        Environment->setFocus(e);
@@ -649,9 +752,8 @@ void GUIFormSpecMenu::parseBackground(parserData* data, const std::string &eleme
 {
        std::vector<std::string> parts = split(element,';');
 
-       if (((parts.size() == 3) || (parts.size() == 4)) ||
-               ((parts.size() > 4) && (m_formspec_version > FORMSPEC_API_VERSION)))
-       {
+       if ((parts.size() >= 3 && parts.size() <= 5) ||
+                       (parts.size() > 5 && m_formspec_version > FORMSPEC_API_VERSION)) {
                std::vector<std::string> v_pos = split(parts[0],',');
                std::vector<std::string> v_geom = split(parts[1],',');
                std::string name = unescape_string(parts[2]);
@@ -659,24 +761,58 @@ void GUIFormSpecMenu::parseBackground(parserData* data, const std::string &eleme
                MY_CHECKPOS("background",0);
                MY_CHECKGEOM("background",1);
 
-               v2s32 pos = padding + AbsoluteRect.UpperLeftCorner + pos_offset * spacing;
-               pos.X += stof(v_pos[0]) * (float)spacing.X - ((float)spacing.X - (float)imgsize.X)/2;
-               pos.Y += stof(v_pos[1]) * (float)spacing.Y - ((float)spacing.Y - (float)imgsize.Y)/2;
-
+               v2s32 pos;
                v2s32 geom;
-               geom.X = stof(v_geom[0]) * (float)spacing.X;
-               geom.Y = stof(v_geom[1]) * (float)spacing.Y;
 
-               if (!data->explicit_size)
-                       warningstream<<"invalid use of background without a size[] element"<<std::endl;
+               if (data->real_coordinates) {
+                       pos = getRealCoordinateBasePos(true, v_pos);
+                       geom = getRealCoordinateGeometry(v_geom);
+               } else {
+                       pos = getElementBasePos(true, &v_pos);
+                       pos.X -= (spacing.X - (float)imgsize.X) / 2;
+                       pos.Y -= (spacing.Y - (float)imgsize.Y) / 2;
+
+                       geom.X = stof(v_geom[0]) * spacing.X;
+                       geom.Y = stof(v_geom[1]) * spacing.Y;
+               }
 
                bool clip = false;
-               if (parts.size() == 4 && is_yes(parts[3])) {
-                       pos.X = stoi(v_pos[0]); //acts as offset
-                       pos.Y = stoi(v_pos[1]); //acts as offset
+               if (parts.size() >= 4 && is_yes(parts[3])) {
+                       if (data->real_coordinates) {
+                               pos = getRealCoordinateBasePos(false, v_pos) * -1;
+                               geom = v2s32(0, 0);
+                       } else {
+                               pos.X = stoi(v_pos[0]); //acts as offset
+                               pos.Y = stoi(v_pos[1]);
+                       }
                        clip = true;
                }
-               m_backgrounds.emplace_back(name, pos, geom, clip);
+
+               core::rect<s32> middle;
+               if (parts.size() >= 5) {
+                       std::vector<std::string> v_middle = split(parts[4], ',');
+                       if (v_middle.size() == 1) {
+                               s32 x = stoi(v_middle[0]);
+                               middle.UpperLeftCorner = core::vector2di(x, x);
+                               middle.LowerRightCorner = core::vector2di(-x, -x);
+                       } else if (v_middle.size() == 2) {
+                               s32 x = stoi(v_middle[0]);
+                               s32 y = stoi(v_middle[1]);
+                               middle.UpperLeftCorner = core::vector2di(x, y);
+                               middle.LowerRightCorner = core::vector2di(-x, -y);
+                               // `-x` is interpreted as `w - x`
+                       } else if (v_middle.size() == 4) {
+                               middle.UpperLeftCorner = core::vector2di(stoi(v_middle[0]), stoi(v_middle[1]));
+                               middle.LowerRightCorner = core::vector2di(stoi(v_middle[2]), stoi(v_middle[3]));
+                       } else {
+                               warningstream << "Invalid rectangle given to middle param of background[] element" << std::endl;
+                       }
+               }
+
+               if (!data->explicit_size && !clip)
+                       warningstream << "invalid use of unclipped background without a size[] element" << std::endl;
+
+               m_backgrounds.emplace_back(name, pos, geom, middle, clip);
 
                return;
        }
@@ -735,13 +871,17 @@ void GUIFormSpecMenu::parseTable(parserData* data, const std::string &element)
                MY_CHECKPOS("table",0);
                MY_CHECKGEOM("table",1);
 
-               v2s32 pos = padding + pos_offset * spacing;
-               pos.X += stof(v_pos[0]) * (float)spacing.X;
-               pos.Y += stof(v_pos[1]) * (float)spacing.Y;
-
+               v2s32 pos;
                v2s32 geom;
-               geom.X = stof(v_geom[0]) * (float)spacing.X;
-               geom.Y = stof(v_geom[1]) * (float)spacing.Y;
+
+               if (data->real_coordinates) {
+                       pos = getRealCoordinateBasePos(false, v_pos);
+                       geom = getRealCoordinateGeometry(v_geom);
+               } else {
+                       pos = getElementBasePos(false, &v_pos);
+                       geom.X = stof(v_geom[0]) * spacing.X;
+                       geom.Y = stof(v_geom[1]) * spacing.Y;
+               }
 
                core::rect<s32> rect = core::rect<s32>(pos.X, pos.Y, pos.X+geom.X, pos.Y+geom.Y);
 
@@ -775,6 +915,9 @@ void GUIFormSpecMenu::parseTable(parserData* data, const std::string &element)
                if (!str_initial_selection.empty() && str_initial_selection != "0")
                        e->setSelected(stoi(str_initial_selection));
 
+               auto style = getStyleForElement("table", name);
+               e->setNotClipped(style.getBool(StyleSpec::NOCLIP, false));
+
                m_tables.emplace_back(spec, e);
                m_fields.push_back(spec);
                return;
@@ -805,14 +948,17 @@ void GUIFormSpecMenu::parseTextList(parserData* data, const std::string &element
                MY_CHECKPOS("textlist",0);
                MY_CHECKGEOM("textlist",1);
 
-               v2s32 pos = padding + pos_offset * spacing;
-               pos.X += stof(v_pos[0]) * (float)spacing.X;
-               pos.Y += stof(v_pos[1]) * (float)spacing.Y;
-
+               v2s32 pos;
                v2s32 geom;
-               geom.X = stof(v_geom[0]) * (float)spacing.X;
-               geom.Y = stof(v_geom[1]) * (float)spacing.Y;
 
+               if (data->real_coordinates) {
+                       pos = getRealCoordinateBasePos(false, v_pos);
+                       geom = getRealCoordinateGeometry(v_geom);
+               } else {
+                       pos = getElementBasePos(false, &v_pos);
+                       geom.X = stof(v_geom[0]) * spacing.X;
+                       geom.Y = stof(v_geom[1]) * spacing.Y;
+               }
 
                core::rect<s32> rect = core::rect<s32>(pos.X, pos.Y, pos.X+geom.X, pos.Y+geom.Y);
 
@@ -846,6 +992,9 @@ void GUIFormSpecMenu::parseTextList(parserData* data, const std::string &element
                if (!str_initial_selection.empty() && str_initial_selection != "0")
                        e->setSelected(stoi(str_initial_selection));
 
+               auto style = getStyleForElement("textlist", name);
+               e->setNotClipped(style.getBool(StyleSpec::NOCLIP, false));
+
                m_tables.emplace_back(spec, e);
                m_fields.push_back(spec);
                return;
@@ -869,14 +1018,29 @@ void GUIFormSpecMenu::parseDropDown(parserData* data, const std::string &element
 
                MY_CHECKPOS("dropdown",0);
 
-               v2s32 pos = padding + pos_offset * spacing;
-               pos.X += stof(v_pos[0]) * (float)spacing.X;
-               pos.Y += stof(v_pos[1]) * (float)spacing.Y;
+               v2s32 pos;
+               v2s32 geom;
+               core::rect<s32> rect;
+
+               if (data->real_coordinates) {
+                       std::vector<std::string> v_geom = split(parts[1],',');
 
-               s32 width = stof(parts[1]) * (float)spacing.Y;
+                       if (v_geom.size() == 1)
+                               v_geom.emplace_back("1");
 
-               core::rect<s32> rect = core::rect<s32>(pos.X, pos.Y,
-                               pos.X + width, pos.Y + (m_btn_height * 2));
+                       MY_CHECKGEOM("dropdown",1);
+
+                       pos = getRealCoordinateBasePos(false, v_pos);
+                       geom = getRealCoordinateGeometry(v_geom);
+                       rect = core::rect<s32>(pos.X, pos.Y, pos.X+geom.X, pos.Y+geom.Y);
+               } else {
+                       pos = getElementBasePos(false, &v_pos);
+
+                       s32 width = stof(parts[1]) * spacing.Y;
+
+                       rect = core::rect<s32>(pos.X, pos.Y,
+                                       pos.X + width, pos.Y + (m_btn_height * 2));
+               }
 
                FieldSpec spec(
                        name,
@@ -903,6 +1067,9 @@ void GUIFormSpecMenu::parseDropDown(parserData* data, const std::string &element
                if (!str_initial_selection.empty())
                        e->setSelected(stoi(str_initial_selection)-1);
 
+               auto style = getStyleForElement("dropdown", name);
+               e->setNotClipped(style.getBool(StyleSpec::NOCLIP, false));
+
                m_fields.push_back(spec);
 
                m_dropdowns.emplace_back(spec, std::vector<std::string>());
@@ -941,16 +1108,22 @@ void GUIFormSpecMenu::parsePwdField(parserData* data, const std::string &element
                MY_CHECKPOS("pwdfield",0);
                MY_CHECKGEOM("pwdfield",1);
 
-               v2s32 pos = pos_offset * spacing;
-               pos.X += stof(v_pos[0]) * (float)spacing.X;
-               pos.Y += stof(v_pos[1]) * (float)spacing.Y;
-
+               v2s32 pos;
                v2s32 geom;
-               geom.X = (stof(v_geom[0]) * (float)spacing.X)-(spacing.X-imgsize.X);
 
-               pos.Y += (stof(v_geom[1]) * (float)imgsize.Y)/2;
-               pos.Y -= m_btn_height;
-               geom.Y = m_btn_height*2;
+               if (data->real_coordinates) {
+                       pos = getRealCoordinateBasePos(false, v_pos);
+                       geom = getRealCoordinateGeometry(v_geom);
+               } else {
+                       pos = getElementBasePos(false, &v_pos);
+                       pos -= padding;
+
+                       geom.X = (stof(v_geom[0]) * spacing.X) - (spacing.X - imgsize.X);
+
+                       pos.Y += (stof(v_geom[1]) * (float)imgsize.Y)/2;
+                       pos.Y -= m_btn_height;
+                       geom.Y = m_btn_height*2;
+               }
 
                core::rect<s32> rect = core::rect<s32>(pos.X, pos.Y, pos.X+geom.X, pos.Y+geom.Y);
 
@@ -980,6 +1153,11 @@ void GUIFormSpecMenu::parsePwdField(parserData* data, const std::string &element
 
                e->setPasswordBox(true,L'*');
 
+               auto style = getStyleForElement("pwdfield", name, "field");
+               e->setNotClipped(style.getBool(StyleSpec::NOCLIP, false));
+               e->setDrawBorder(style.getBool(StyleSpec::BORDER, true));
+               e->setOverrideColor(style.getColor(StyleSpec::TEXTCOLOR, video::SColor(0xFFFFFFFF)));
+
                irr::SEvent evt;
                evt.EventType            = EET_KEY_INPUT_EVENT;
                evt.KeyInput.Key         = KEY_END;
@@ -1039,7 +1217,7 @@ void GUIFormSpecMenu::createTextField(parserData *data, FieldSpec &spec,
        }
 
        if (e) {
-               if (is_editable && spec.fname == data->focused_fieldname) 
+               if (is_editable && spec.fname == data->focused_fieldname)
                        Environment->setFocus(e);
 
                if (is_multiline) {
@@ -1056,6 +1234,14 @@ void GUIFormSpecMenu::createTextField(parserData *data, FieldSpec &spec,
                        evt.KeyInput.PressedDown = true;
                        e->OnEvent(evt);
                }
+
+               auto style = getStyleForElement(is_multiline ? "textarea" : "field", spec.fname);
+               e->setNotClipped(style.getBool(StyleSpec::NOCLIP, false));
+               e->setDrawBorder(style.getBool(StyleSpec::BORDER, true));
+               e->setOverrideColor(style.getColor(StyleSpec::TEXTCOLOR, video::SColor(0xFFFFFFFF)));
+               if (style.get(StyleSpec::BGCOLOR, "") == "transparent") {
+                       e->setDrawBackground(false);
+               }
        }
 
        if (!spec.flabel.empty()) {
@@ -1079,7 +1265,7 @@ void GUIFormSpecMenu::parseSimpleField(parserData* data,
        if(data->explicit_size)
                warningstream<<"invalid use of unpositioned \"field\" in inventory"<<std::endl;
 
-       v2s32 pos = padding + AbsoluteRect.UpperLeftCorner + pos_offset * spacing;
+       v2s32 pos = getElementBasePos(false, nullptr);
        pos.Y = ((m_fields.size()+2)*60);
        v2s32 size = DesiredRect.getSize();
 
@@ -1125,24 +1311,29 @@ void GUIFormSpecMenu::parseTextArea(parserData* data, std::vector<std::string>&
        MY_CHECKPOS(type,0);
        MY_CHECKGEOM(type,1);
 
-       v2s32 pos = pos_offset * spacing;
-       pos.X += stof(v_pos[0]) * (float) spacing.X;
-       pos.Y += stof(v_pos[1]) * (float) spacing.Y;
-
+       v2s32 pos;
        v2s32 geom;
 
-       geom.X = (stof(v_geom[0]) * (float)spacing.X)-(spacing.X-imgsize.X);
+       if (data->real_coordinates) {
+               pos = getRealCoordinateBasePos(false, v_pos);
+               geom = getRealCoordinateGeometry(v_geom);
+       } else {
+               pos = getElementBasePos(false, &v_pos);
+               pos -= padding;
 
-       if (type == "textarea")
-       {
-               geom.Y = (stof(v_geom[1]) * (float)imgsize.Y) - (spacing.Y-imgsize.Y);
-               pos.Y += m_btn_height;
-       }
-       else
-       {
-               pos.Y += (stof(v_geom[1]) * (float)imgsize.Y)/2;
-               pos.Y -= m_btn_height;
-               geom.Y = m_btn_height*2;
+               geom.X = (stof(v_geom[0]) * spacing.X) - (spacing.X - imgsize.X);
+
+               if (type == "textarea")
+               {
+                       geom.Y = (stof(v_geom[1]) * (float)imgsize.Y) - (spacing.Y-imgsize.Y);
+                       pos.Y += m_btn_height;
+               }
+               else
+               {
+                       pos.Y += (stof(v_geom[1]) * (float)imgsize.Y)/2;
+                       pos.Y -= m_btn_height;
+                       geom.Y = m_btn_height*2;
+               }
        }
 
        core::rect<s32> rect = core::rect<s32>(pos.X, pos.Y, pos.X+geom.X, pos.Y+geom.Y);
@@ -1206,31 +1397,56 @@ void GUIFormSpecMenu::parseLabel(parserData* data, const std::string &element)
 
                MY_CHECKPOS("label",0);
 
-               v2s32 pos = padding + pos_offset * spacing;
-               pos.X += stof(v_pos[0]) * (float)spacing.X;
-               pos.Y += (stof(v_pos[1]) + 7.0/30.0) * (float)spacing.Y;
-
                if(!data->explicit_size)
                        warningstream<<"invalid use of label without a size[] element"<<std::endl;
 
                std::vector<std::string> lines = split(text, '\n');
 
                for (unsigned int i = 0; i != lines.size(); i++) {
-                       // Lines are spaced at the nominal distance of
-                       // 2/5 inventory slot, even if the font doesn't
-                       // quite match that.  This provides consistent
-                       // form layout, at the expense of sometimes
-                       // having sub-optimal spacing for the font.
-                       // We multiply by 2 and then divide by 5, rather
-                       // than multiply by 0.4, to get exact results
-                       // in the integer cases: 0.4 is not exactly
-                       // representable in binary floating point.
-                       s32 posy = pos.Y + ((float)i) * spacing.Y * 2.0 / 5.0;
-                       std::wstring wlabel = utf8_to_wide(unescape_string(lines[i]));
-                       core::rect<s32> rect = core::rect<s32>(
-                               pos.X, posy - m_btn_height,
-                               pos.X + m_font->getDimension(wlabel.c_str()).Width,
-                               posy + m_btn_height);
+                       std::wstring wlabel = unescape_translate(unescape_string(
+                               utf8_to_wide(lines[i])));
+
+                       core::rect<s32> rect;
+
+                       if (data->real_coordinates) {
+                               // Lines are spaced at the distance of 1/2 imgsize.
+                               // This alows lines that line up with the new elements
+                               // easily without sacrificing good line distance.  If
+                               // it was one whole imgsize, it would have too much
+                               // spacing.
+                               v2s32 pos = getRealCoordinateBasePos(false, v_pos);
+
+                               // Labels are positioned by their center, not their top.
+                               pos.Y += (((float) imgsize.Y) / -2) + (((float) imgsize.Y) * i / 2);
+
+                               rect = core::rect<s32>(
+                                       pos.X, pos.Y,
+                                       pos.X + m_font->getDimension(wlabel.c_str()).Width,
+                                       pos.Y + imgsize.Y);
+
+                       } else {
+                               // Lines are spaced at the nominal distance of
+                               // 2/5 inventory slot, even if the font doesn't
+                               // quite match that.  This provides consistent
+                               // form layout, at the expense of sometimes
+                               // having sub-optimal spacing for the font.
+                               // We multiply by 2 and then divide by 5, rather
+                               // than multiply by 0.4, to get exact results
+                               // in the integer cases: 0.4 is not exactly
+                               // representable in binary floating point.
+
+                               v2s32 pos = getElementBasePos(false, nullptr);
+                               pos.X += stof(v_pos[0]) * spacing.X;
+                               pos.Y += (stof(v_pos[1]) + 7.0f / 30.0f) * spacing.Y;
+
+                               pos.Y += ((float) i) * spacing.Y * 2.0 / 5.0;
+
+                               rect = core::rect<s32>(
+                                       pos.X, pos.Y - m_btn_height,
+                                       pos.X + m_font->getDimension(wlabel.c_str()).Width,
+                                       pos.Y + m_btn_height);
+                       }
+
                        FieldSpec spec(
                                "",
                                wlabel,
@@ -1240,12 +1456,18 @@ void GUIFormSpecMenu::parseLabel(parserData* data, const std::string &element)
                        gui::IGUIStaticText *e = gui::StaticText::add(Environment,
                                spec.flabel.c_str(), rect, false, false, this, spec.fid);
                        e->setTextAlignment(gui::EGUIA_UPPERLEFT, gui::EGUIA_CENTER);
+
+                       auto style = getStyleForElement("label", spec.fname);
+                       e->setNotClipped(style.getBool(StyleSpec::NOCLIP, false));
+                       e->setOverrideColor(style.getColor(StyleSpec::TEXTCOLOR, video::SColor(0xFFFFFFFF)));
+
                        m_fields.push_back(spec);
                }
 
                return;
        }
-       errorstream<< "Invalid label element(" << parts.size() << "): '" << element << "'"  << std::endl;
+       errorstream << "Invalid label element(" << parts.size() << "): '" << element
+               << "'"  << std::endl;
 }
 
 void GUIFormSpecMenu::parseVertLabel(parserData* data, const std::string &element)
@@ -1261,17 +1483,35 @@ void GUIFormSpecMenu::parseVertLabel(parserData* data, const std::string &elemen
 
                MY_CHECKPOS("vertlabel",1);
 
-               v2s32 pos = padding + pos_offset * spacing;
-               pos.X += stof(v_pos[0]) * (float)spacing.X;
-               pos.Y += stof(v_pos[1]) * (float)spacing.Y;
+               v2s32 pos;
+               core::rect<s32> rect;
+
+               if (data->real_coordinates) {
+                       pos = getRealCoordinateBasePos(false, v_pos);
+
+                       // Vertlabels are positioned by center, not left.
+                       pos.X -= imgsize.X / 2;
 
-               core::rect<s32> rect = core::rect<s32>(
-                               pos.X, pos.Y+((imgsize.Y/2)- m_btn_height),
+                       // We use text.length + 1 because without it, the rect
+                       // isn't quite tall enough and cuts off the text.
+                       rect = core::rect<s32>(pos.X, pos.Y,
+                               pos.X + imgsize.X,
+                               pos.Y + font_line_height(m_font) *
+                               (text.length() + 1));
+
+               } else {
+                       pos = getElementBasePos(false, &v_pos);
+
+                       // As above, the length must be one longer. The width of
+                       // the rect (15 pixels) seems rather arbitrary, but
+                       // changing it might break something.
+                       rect = core::rect<s32>(
+                               pos.X, pos.Y+((imgsize.Y/2) - m_btn_height),
                                pos.X+15, pos.Y +
-                                       font_line_height(m_font)
-                                       * (text.length()+1)
-                                       +((imgsize.Y/2)- m_btn_height));
-               //actually text.length() would be correct but adding +1 avoids to break all mods
+                                       font_line_height(m_font) *
+                                       (text.length() + 1) +
+                                       ((imgsize.Y/2) - m_btn_height));
+               }
 
                if(!data->explicit_size)
                        warningstream<<"invalid use of label without a size[] element"<<std::endl;
@@ -1289,9 +1529,14 @@ void GUIFormSpecMenu::parseVertLabel(parserData* data, const std::string &elemen
                        L"",
                        258+m_fields.size()
                );
-               gui::IGUIStaticText *t = gui::StaticText::add(Environment, spec.flabel.c_str(),
+               gui::IGUIStaticText *e = gui::StaticText::add(Environment, spec.flabel.c_str(),
                        rect, false, false, this, spec.fid);
-               t->setTextAlignment(gui::EGUIA_CENTER, gui::EGUIA_CENTER);
+               e->setTextAlignment(gui::EGUIA_CENTER, gui::EGUIA_CENTER);
+
+               auto style = getStyleForElement("vertlabel", spec.fname, "label");
+               e->setNotClipped(style.getBool(StyleSpec::NOCLIP, false));
+               e->setOverrideColor(style.getColor(StyleSpec::TEXTCOLOR, video::SColor(0xFFFFFFFF)));
+
                m_fields.push_back(spec);
                return;
        }
@@ -1315,13 +1560,6 @@ void GUIFormSpecMenu::parseImageButton(parserData* data, const std::string &elem
                MY_CHECKPOS("imagebutton",0);
                MY_CHECKGEOM("imagebutton",1);
 
-               v2s32 pos = padding + pos_offset * spacing;
-               pos.X += stof(v_pos[0]) * (float)spacing.X;
-               pos.Y += stof(v_pos[1]) * (float)spacing.Y;
-               v2s32 geom;
-               geom.X = (stof(v_geom[0]) * (float)spacing.X)-(spacing.X-imgsize.X);
-               geom.Y = (stof(v_geom[1]) * (float)spacing.Y)-(spacing.Y-imgsize.Y);
-
                bool noclip     = false;
                bool drawborder = true;
                std::string pressed_image_name;
@@ -1337,9 +1575,22 @@ void GUIFormSpecMenu::parseImageButton(parserData* data, const std::string &elem
                        pressed_image_name = parts[7];
                }
 
-               core::rect<s32> rect = core::rect<s32>(pos.X, pos.Y, pos.X+geom.X, pos.Y+geom.Y);
+               v2s32 pos;
+               v2s32 geom;
 
-               if(!data->explicit_size)
+               if (data->real_coordinates) {
+                       pos = getRealCoordinateBasePos(false, v_pos);
+                       geom = getRealCoordinateGeometry(v_geom);
+               } else {
+                       pos = getElementBasePos(false, &v_pos);
+                       geom.X = (stof(v_geom[0]) * spacing.X) - (spacing.X - imgsize.X);
+                       geom.Y = (stof(v_geom[1]) * spacing.Y) - (spacing.Y - imgsize.Y);
+               }
+
+               core::rect<s32> rect = core::rect<s32>(pos.X, pos.Y, pos.X+geom.X,
+                       pos.Y+geom.Y);
+
+               if (!data->explicit_size)
                        warningstream<<"invalid use of image_button without a size[] element"<<std::endl;
 
                image_name = unescape_string(image_name);
@@ -1354,7 +1605,7 @@ void GUIFormSpecMenu::parseImageButton(parserData* data, const std::string &elem
                        258+m_fields.size()
                );
                spec.ftype = f_Button;
-               if(type == "image_button_exit")
+               if (type == "image_button_exit")
                        spec.is_exit = true;
 
                video::ITexture *texture = 0;
@@ -1371,14 +1622,21 @@ void GUIFormSpecMenu::parseImageButton(parserData* data, const std::string &elem
                        Environment->setFocus(e);
                }
 
-               e->setUseAlphaChannel(true);
+               auto style = getStyleForElement("image_button", spec.fname);
+
+               e->setUseAlphaChannel(style.getBool(StyleSpec::ALPHA, true));
                e->setImage(guiScalingImageButton(
                        Environment->getVideoDriver(), texture, geom.X, geom.Y));
                e->setPressedImage(guiScalingImageButton(
                        Environment->getVideoDriver(), pressed_texture, geom.X, geom.Y));
                e->setScaleImage(true);
-               e->setNotClipped(noclip);
-               e->setDrawBorder(drawborder);
+               if (parts.size() >= 7) {
+                       e->setNotClipped(noclip);
+                       e->setDrawBorder(drawborder);
+               } else {
+                       e->setNotClipped(style.getBool(StyleSpec::NOCLIP, false));
+                       e->setDrawBorder(style.getBool(StyleSpec::BORDER, true));
+               }
 
                m_fields.push_back(spec);
                return;
@@ -1389,25 +1647,43 @@ void GUIFormSpecMenu::parseImageButton(parserData* data, const std::string &elem
 
 void GUIFormSpecMenu::parseTabHeader(parserData* data, const std::string &element)
 {
-       std::vector<std::string> parts = split(element,';');
+       std::vector<std::string> parts = split(element, ';');
 
-       if (((parts.size() == 4) || (parts.size() == 6)) ||
-               ((parts.size() > 6) && (m_formspec_version > FORMSPEC_API_VERSION)))
+       if (((parts.size() == 4) || (parts.size() == 6)) || (parts.size() == 7 &&
+               data->real_coordinates) || ((parts.size() > 6) &&
+               (m_formspec_version > FORMSPEC_API_VERSION)))
        {
                std::vector<std::string> v_pos = split(parts[0],',');
-               std::string name = parts[1];
-               std::vector<std::string> buttons = split(parts[2],',');
-               std::string str_index = parts[3];
+
+               // If we're using real coordinates, add an extra field for height.
+               // Width is not here because tabs are the width of the text, and
+               // there's no reason to change that.
+               unsigned int i = 0;
+               std::vector<std::string> v_geom = {"1", "0.75"}; // Dummy width and default height
+               bool auto_width = true;
+               if (parts.size() == 7) {
+                       i++;
+
+                       v_geom = split(parts[1], ',');
+                       if (v_geom.size() == 1)
+                               v_geom.insert(v_geom.begin(), "1"); // Dummy value
+                       else
+                               auto_width = false;
+               }
+
+               std::string name = parts[i+1];
+               std::vector<std::string> buttons = split(parts[i+2], ',');
+               std::string str_index = parts[i+3];
                bool show_background = true;
                bool show_border = true;
-               int tab_index = stoi(str_index) -1;
+               int tab_index = stoi(str_index) - 1;
 
-               MY_CHECKPOS("tabheader",0);
+               MY_CHECKPOS("tabheader", 0);
 
-               if (parts.size() == 6) {
-                       if (parts[4] == "true")
+               if (parts.size() == 6 + i) {
+                       if (parts[4+i] == "true")
                                show_background = false;
-                       if (parts[5] == "false")
+                       if (parts[5+i] == "false")
                                show_border = false;
                }
 
@@ -1420,12 +1696,27 @@ void GUIFormSpecMenu::parseTabHeader(parserData* data, const std::string &elemen
 
                spec.ftype = f_TabHeader;
 
-               v2s32 pos = pos_offset * spacing;
-               pos.X += stof(v_pos[0]) * (float)spacing.X;
-               pos.Y += stof(v_pos[1]) * (float)spacing.Y - m_btn_height * 2;
+               v2s32 pos;
                v2s32 geom;
-               geom.X = DesiredRect.getWidth();
-               geom.Y = m_btn_height*2;
+
+               if (data->real_coordinates) {
+                       pos = getRealCoordinateBasePos(false, v_pos);
+
+                       geom = getRealCoordinateGeometry(v_geom);
+                       pos.Y -= geom.Y; // TabHeader base pos is the bottom, not the top.
+                       if (auto_width)
+                               geom.X = DesiredRect.getWidth(); // Set automatic width
+
+                       MY_CHECKGEOM("tabheader", 1);
+               } else {
+                       v2f32 pos_f = pos_offset * spacing;
+                       pos_f.X += stof(v_pos[0]) * spacing.X;
+                       pos_f.Y += stof(v_pos[1]) * spacing.Y - m_btn_height * 2;
+                       pos = v2s32(pos_f.X, pos_f.Y);
+
+                       geom.Y = m_btn_height * 2;
+                       geom.X = DesiredRect.getWidth();
+               }
 
                core::rect<s32> rect = core::rect<s32>(pos.X, pos.Y, pos.X+geom.X,
                                pos.Y+geom.Y);
@@ -1434,17 +1725,22 @@ void GUIFormSpecMenu::parseTabHeader(parserData* data, const std::string &elemen
                                show_background, show_border, spec.fid);
                e->setAlignment(irr::gui::EGUIA_UPPERLEFT, irr::gui::EGUIA_UPPERLEFT,
                                irr::gui::EGUIA_UPPERLEFT, irr::gui::EGUIA_LOWERRIGHT);
-               e->setTabHeight(m_btn_height*2);
+               e->setTabHeight(geom.Y);
 
                if (spec.fname == data->focused_fieldname) {
                        Environment->setFocus(e);
                }
 
-               e->setNotClipped(true);
+               auto style = getStyleForElement("tabheader", name);
+               e->setNotClipped(style.getBool(StyleSpec::NOCLIP, true));
 
                for (const std::string &button : buttons) {
-                       e->addTab(unescape_translate(unescape_string(
+                       auto tab = e->addTab(unescape_translate(unescape_string(
                                utf8_to_wide(button))).c_str(), -1);
+                       if (style.isNotDefault(StyleSpec::BGCOLOR))
+                               tab->setBackgroundColor(style.getColor(StyleSpec::BGCOLOR));
+
+                       tab->setTextColor(style.getColor(StyleSpec::TEXTCOLOR, video::SColor(0xFFFFFFFF)));
                }
 
                if ((tab_index >= 0) &&
@@ -1485,12 +1781,17 @@ void GUIFormSpecMenu::parseItemImageButton(parserData* data, const std::string &
                MY_CHECKPOS("itemimagebutton",0);
                MY_CHECKGEOM("itemimagebutton",1);
 
-               v2s32 pos = padding + pos_offset * spacing;
-               pos.X += stof(v_pos[0]) * (float)spacing.X;
-               pos.Y += stof(v_pos[1]) * (float)spacing.Y;
+               v2s32 pos;
                v2s32 geom;
-               geom.X = (stof(v_geom[0]) * (float)spacing.X)-(spacing.X-imgsize.X);
-               geom.Y = (stof(v_geom[1]) * (float)spacing.Y)-(spacing.Y-imgsize.Y);
+
+               if (data->real_coordinates) {
+                       pos = getRealCoordinateBasePos(false, v_pos);
+                       geom = getRealCoordinateGeometry(v_geom);
+               } else {
+                       pos = getElementBasePos(false, &v_pos);
+                       geom.X = (stof(v_geom[0]) * spacing.X) - (spacing.X - imgsize.X);
+                       geom.Y = (stof(v_geom[1]) * spacing.Y) - (spacing.Y - imgsize.Y);
+               }
 
                core::rect<s32> rect = core::rect<s32>(pos.X, pos.Y, pos.X+geom.X, pos.Y+geom.Y);
 
@@ -1515,6 +1816,10 @@ void GUIFormSpecMenu::parseItemImageButton(parserData* data, const std::string &
 
                gui::IGUIButton *e = Environment->addButton(rect, this, spec.fid, L"");
 
+               auto style = getStyleForElement("item_image_button", spec.fname, "image_button");
+               e->setNotClipped(style.getBool(StyleSpec::NOCLIP, false));
+               e->setDrawBorder(style.getBool(StyleSpec::BORDER, true));
+
                if (spec.fname == data->focused_fieldname) {
                        Environment->setFocus(e);
                }
@@ -1523,9 +1828,12 @@ void GUIFormSpecMenu::parseItemImageButton(parserData* data, const std::string &
                rect+=data->basepos-padding;
                spec.rect=rect;
                m_fields.push_back(spec);
-               pos = padding + AbsoluteRect.UpperLeftCorner + pos_offset * spacing;
-               pos.X += stof(v_pos[0]) * (float) spacing.X;
-               pos.Y += stof(v_pos[1]) * (float) spacing.Y;
+
+               if (data->real_coordinates)
+                       pos = getRealCoordinateBasePos(true, v_pos);
+               else
+                       pos = getElementBasePos(true, &v_pos);
+
                m_itemimages.emplace_back("", item_name, e, pos, geom);
                m_static_texts.emplace_back(utf8_to_wide(label), rect, e);
                return;
@@ -1546,17 +1854,21 @@ void GUIFormSpecMenu::parseBox(parserData* data, const std::string &element)
                MY_CHECKPOS("box",0);
                MY_CHECKGEOM("box",1);
 
-               v2s32 pos = padding + AbsoluteRect.UpperLeftCorner + pos_offset * spacing;
-               pos.X += stof(v_pos[0]) * (float) spacing.X;
-               pos.Y += stof(v_pos[1]) * (float) spacing.Y;
-
+               v2s32 pos;
                v2s32 geom;
-               geom.X = stof(v_geom[0]) * (float)spacing.X;
-               geom.Y = stof(v_geom[1]) * (float)spacing.Y;
+
+               if (data->real_coordinates) {
+                       pos = getRealCoordinateBasePos(true, v_pos);
+                       geom = getRealCoordinateGeometry(v_geom);
+               } else {
+                       pos = getElementBasePos(true, &v_pos);
+                       geom.X = stof(v_geom[0]) * spacing.X;
+                       geom.Y = stof(v_geom[1]) * spacing.Y;
+               }
 
                video::SColor tmp_color;
 
-               if (parseColorString(parts[2], tmp_color, false)) {
+               if (parseColorString(parts[2], tmp_color, false, 0x8C)) {
                        BoxDrawSpec spec(pos, geom, tmp_color);
 
                        m_boxes.push_back(spec);
@@ -1620,23 +1932,61 @@ void GUIFormSpecMenu::parseListColors(parserData* data, const std::string &eleme
 void GUIFormSpecMenu::parseTooltip(parserData* data, const std::string &element)
 {
        std::vector<std::string> parts = split(element,';');
-       if (parts.size() == 2) {
-               std::string name = parts[0];
-               m_tooltips[name] = TooltipSpec(utf8_to_wide(unescape_string(parts[1])),
-                       m_default_tooltip_bgcolor, m_default_tooltip_color);
+       if (parts.size() < 2) {
+               errorstream << "Invalid tooltip element(" << parts.size() << "): '"
+                               << element << "'"  << std::endl;
                return;
        }
 
-       if (parts.size() == 4) {
-               std::string name = parts[0];
-               video::SColor tmp_color1, tmp_color2;
-               if ( parseColorString(parts[2], tmp_color1, false) && parseColorString(parts[3], tmp_color2, false) ) {
-                       m_tooltips[name] = TooltipSpec(utf8_to_wide(unescape_string(parts[1])),
-                               tmp_color1, tmp_color2);
-                       return;
+       // Get mode and check size
+       bool rect_mode = parts[0].find(',') != std::string::npos;
+       size_t base_size = rect_mode ? 3 : 2;
+       if (parts.size() != base_size && parts.size() != base_size + 2) {
+               errorstream << "Invalid tooltip element(" << parts.size() << "): '"
+                               << element << "'"  << std::endl;
+               return;
+       }
+
+       // Read colors
+       video::SColor bgcolor = m_default_tooltip_bgcolor;
+       video::SColor color   = m_default_tooltip_color;
+       if (parts.size() == base_size + 2 &&
+                       (!parseColorString(parts[base_size], bgcolor, false) ||
+                               !parseColorString(parts[base_size + 1], color, false))) {
+               errorstream << "Invalid color in tooltip element(" << parts.size()
+                               << "): '" << element << "'"  << std::endl;
+               return;
+       }
+
+       // Make tooltip spec
+       std::string text = unescape_string(parts[rect_mode ? 2 : 1]);
+       TooltipSpec spec(utf8_to_wide(text), bgcolor, color);
+
+       // Add tooltip
+       if (rect_mode) {
+               std::vector<std::string> v_pos  = split(parts[0], ',');
+               std::vector<std::string> v_geom = split(parts[1], ',');
+
+               MY_CHECKPOS("tooltip", 0);
+               MY_CHECKGEOM("tooltip", 1);
+
+               v2s32 pos;
+               v2s32 geom;
+
+               if (data->real_coordinates) {
+                       pos = getRealCoordinateBasePos(true, v_pos);
+                       geom = getRealCoordinateGeometry(v_geom);
+               } else {
+                       pos = getElementBasePos(true, &v_pos);
+                       geom.X = stof(v_geom[0]) * spacing.X;
+                       geom.Y = stof(v_geom[1]) * spacing.Y;
                }
+
+               irr::core::rect<s32> rect(pos, pos + geom);
+               m_tooltip_rects.emplace_back(rect, spec);
+       } else {
+               m_tooltips[parts[0]] = spec;
        }
-       errorstream<< "Invalid tooltip element(" << parts.size() << "): '" << element << "'"  << std::endl;
 }
 
 bool GUIFormSpecMenu::parseVersionDirect(const std::string &data)
@@ -1756,12 +2106,70 @@ void GUIFormSpecMenu::parseAnchor(parserData *data, const std::string &element)
                        << "'" << std::endl;
 }
 
+bool GUIFormSpecMenu::parseStyle(parserData *data, const std::string &element, bool style_type)
+{
+       std::vector<std::string> parts = split(element, ';');
+
+       if (parts.size() < 2) {
+               errorstream << "Invalid style element (" << parts.size() << "): '" << element
+                                       << "'" << std::endl;
+               return false;
+       }
+
+       std::string selector = trim(parts[0]);
+       if (selector.empty()) {
+               errorstream << "Invalid style element (Selector required): '" << element
+                                       << "'" << std::endl;
+               return false;
+       }
+
+       StyleSpec spec;
+
+       for (size_t i = 1; i < parts.size(); i++) {
+               size_t equal_pos = parts[i].find('=');
+               if (equal_pos == std::string::npos) {
+                       errorstream << "Invalid style element (Property missing value): '" << element
+                                               << "'" << std::endl;
+                       return false;
+               }
+
+               std::string propname = trim(parts[i].substr(0, equal_pos));
+               std::string value    = trim(unescape_string(parts[i].substr(equal_pos + 1)));
+
+               std::transform(propname.begin(), propname.end(), propname.begin(), ::tolower);
+
+               StyleSpec::Property prop = StyleSpec::GetPropertyByName(propname);
+               if (prop == StyleSpec::NONE) {
+                       if (property_warned.find(propname) != property_warned.end()) {
+                               warningstream << "Invalid style element (Unknown property " << propname << "): '"
+                                               << element
+                                               << "'" << std::endl;
+                               property_warned.insert(propname);
+                       }
+                       return false;
+               }
+
+               spec.set(prop, value);
+       }
+
+       if (style_type) {
+               theme_by_type[selector] |= spec;
+       } else {
+               theme_by_name[selector] |= spec;
+       }
+
+       return true;
+}
+
 void GUIFormSpecMenu::parseElement(parserData* data, const std::string &element)
 {
        //some prechecks
        if (element.empty())
                return;
 
+       if (parseVersionDirect(element))
+               return;
+
        std::vector<std::string> parts = split(element,'[');
 
        // ugly workaround to keep compatibility
@@ -1916,6 +2324,21 @@ void GUIFormSpecMenu::parseElement(parserData* data, const std::string &element)
                return;
        }
 
+       if (type == "real_coordinates") {
+               data->real_coordinates = is_yes(description);
+               return;
+       }
+
+       if (type == "style") {
+               parseStyle(data, description, false);
+               return;
+       }
+
+       if (type == "style_type") {
+               parseStyle(data, description, true);
+               return;
+       }
+
        // Ignore others
        infostream << "Unknown DrawSpec: type=" << type << ", data=\"" << description << "\""
                        << std::endl;
@@ -1982,9 +2405,12 @@ void GUIFormSpecMenu::regenerateGui(v2u32 screensize)
        m_fields.clear();
        m_boxes.clear();
        m_tooltips.clear();
+       m_tooltip_rects.clear();
        m_inventory_rings.clear();
        m_static_texts.clear();
        m_dropdowns.clear();
+       theme_by_name.clear();
+       theme_by_type.clear();
 
        m_bgfullscreen = false;
 
@@ -2008,7 +2434,6 @@ void GUIFormSpecMenu::regenerateGui(v2u32 screensize)
                );
        }
 
-
        m_slotbg_n = video::SColor(255,128,128,128);
        m_slotbg_h = video::SColor(255,192,192,192);
 
@@ -2040,7 +2465,7 @@ void GUIFormSpecMenu::regenerateGui(v2u32 screensize)
 
        /* try to read version from first element only */
        if (!elements.empty()) {
-               if ( parseVersionDirect(elements[0]) ) {
+               if (parseVersionDirect(elements[0])) {
                        i++;
                }
        }
@@ -2067,6 +2492,29 @@ void GUIFormSpecMenu::regenerateGui(v2u32 screensize)
                }
        }
 
+       /* "no_prepend" element is always after "position" (or  "size" element) if it used */
+       bool enable_prepends = true;
+       for (; i < elements.size(); i++) {
+               if (elements[i].empty())
+                       break;
+
+               std::vector<std::string> parts = split(elements[i], '[');
+               if (trim(parts[0]) == "no_prepend")
+                       enable_prepends = false;
+               else
+                       break;
+       }
+
+       /* Copy of the "real_coordinates" element for after the form size. */
+       mydata.real_coordinates = m_formspec_version >= 2;
+       for (; i < elements.size(); i++) {
+               std::vector<std::string> parts = split(elements[i], '[');
+               std::string name = trim(parts[0]);
+               if (name != "real_coordinates" || parts.size() != 2)
+                       break; // Invalid format
+
+               mydata.real_coordinates = is_yes(trim(parts[1]));
+       }
 
        if (mydata.explicit_size) {
                // compute scaling for specified form size
@@ -2119,16 +2567,28 @@ void GUIFormSpecMenu::regenerateGui(v2u32 screensize)
                        // the image size can't be less than 0.3 inch
                        // multiplied by gui_scaling, even if this means
                        // the form doesn't fit the screen.
-                       double prefer_imgsize = mydata.screensize.Y / 15 *
-                                                       gui_scaling;
+#ifdef __ANDROID__
+                       // For mobile devices these magic numbers are
+                       // different and forms should always use the
+                       // maximum screen space available.
+                       double prefer_imgsize = mydata.screensize.Y / 10 * gui_scaling;
+                       double fitx_imgsize = mydata.screensize.X /
+                               ((12.0 / 8.0) * (0.5 + mydata.invsize.X));
+                       double fity_imgsize = mydata.screensize.Y /
+                               ((15.0 / 11.0) * (0.85 + mydata.invsize.Y));
+                       use_imgsize = MYMIN(prefer_imgsize,
+                                       MYMIN(fitx_imgsize, fity_imgsize));
+#else
+                       double prefer_imgsize = mydata.screensize.Y / 15 * gui_scaling;
                        double fitx_imgsize = mydata.screensize.X /
-                               ((5.0/4.0) * (0.5 + mydata.invsize.X));
+                               ((5.0 / 4.0) * (0.5 + mydata.invsize.X));
                        double fity_imgsize = mydata.screensize.Y /
-                               ((15.0/13.0) * (0.85 * mydata.invsize.Y));
+                               ((15.0 / 13.0) * (0.85 * mydata.invsize.Y));
                        double screen_dpi = RenderingEngine::getDisplayDensity() * 96;
                        double min_imgsize = 0.3 * screen_dpi * gui_scaling;
                        use_imgsize = MYMAX(min_imgsize, MYMIN(prefer_imgsize,
                                MYMIN(fitx_imgsize, fity_imgsize)));
+#endif
                }
 
                // Everything else is scaled in proportion to the
@@ -2140,16 +2600,24 @@ void GUIFormSpecMenu::regenerateGui(v2u32 screensize)
                // is 2/5 vertical inventory slot spacing, and button
                // half-height is 7/8 of font height.
                imgsize = v2s32(use_imgsize, use_imgsize);
-               spacing = v2s32(use_imgsize*5.0/4, use_imgsize*15.0/13);
+               spacing = v2f32(use_imgsize*5.0/4, use_imgsize*15.0/13);
                padding = v2s32(use_imgsize*3.0/8, use_imgsize*3.0/8);
                m_btn_height = use_imgsize*15.0/13 * 0.35;
 
                m_font = g_fontengine->getFont();
 
-               mydata.size = v2s32(
-                       padding.X*2+spacing.X*(mydata.invsize.X-1.0)+imgsize.X,
-                       padding.Y*2+spacing.Y*(mydata.invsize.Y-1.0)+imgsize.Y + m_btn_height*2.0/3.0
-               );
+               if (mydata.real_coordinates) {
+                       mydata.size = v2s32(
+                               mydata.invsize.X*imgsize.X,
+                               mydata.invsize.Y*imgsize.Y
+                       );
+               } else {
+                       mydata.size = v2s32(
+                               padding.X*2+spacing.X*(mydata.invsize.X-1.0)+imgsize.X,
+                               padding.Y*2+spacing.Y*(mydata.invsize.Y-1.0)+imgsize.Y + m_btn_height*2.0/3.0
+                       );
+               }
+
                DesiredRect = mydata.rect = core::rect<s32>(
                                (s32)((f32)mydata.screensize.X * mydata.offset.X) - (s32)(mydata.anchor.X * (f32)mydata.size.X) + offset.X,
                                (s32)((f32)mydata.screensize.Y * mydata.offset.Y) - (s32)(mydata.anchor.Y * (f32)mydata.size.Y) + offset.Y,
@@ -2173,12 +2641,27 @@ void GUIFormSpecMenu::regenerateGui(v2u32 screensize)
        mydata.basepos = getBasePos();
        m_tooltip_element->setOverrideFont(m_font);
 
-       gui::IGUISkinskin = Environment->getSkin();
+       gui::IGUISkin *skin = Environment->getSkin();
        sanity_check(skin);
        gui::IGUIFont *old_font = skin->getFont();
        skin->setFont(m_font);
 
-       pos_offset = v2s32();
+       pos_offset = v2f32();
+
+       if (enable_prepends) {
+               // Backup the coordinates so that prepends can use the coordinates of choice.
+               bool rc_backup = mydata.real_coordinates;
+               bool version_backup = m_formspec_version;
+               mydata.real_coordinates = false; // Old coordinates by default.
+
+               std::vector<std::string> prepend_elements = split(m_formspec_prepend, ']');
+               for (const auto &element : prepend_elements)
+                       parseElement(&mydata, element);
+
+               m_formspec_version = version_backup;
+               mydata.real_coordinates = rc_backup; // Restore coordinates
+       }
+
        for (; i< elements.size(); i++) {
                parseElement(&mydata, elements[i]);
        }
@@ -2229,23 +2712,11 @@ void GUIFormSpecMenu::regenerateGui(v2u32 screensize)
 #ifdef __ANDROID__
 bool GUIFormSpecMenu::getAndroidUIInput()
 {
-       /* no dialog shown */
-       if (m_JavaDialogFieldName == "") {
+       if (!hasAndroidUIInput())
                return false;
-       }
 
-       /* still waiting */
-       if (porting::getInputDialogState() == -1) {
-               return true;
-       }
-
-       std::string fieldname = m_JavaDialogFieldName;
-       m_JavaDialogFieldName = "";
-
-       /* no value abort dialog processing */
-       if (porting::getInputDialogState() != 0) {
-               return false;
-       }
+       std::string fieldname = m_jni_field_name;
+       m_jni_field_name.clear();
 
        for(std::vector<FieldSpec>::iterator iter =  m_fields.begin();
                        iter != m_fields.end(); ++iter) {
@@ -2265,8 +2736,7 @@ bool GUIFormSpecMenu::getAndroidUIInput()
 
                std::string text = porting::getInputDialogValue();
 
-               ((gui::IGUIEditBox*) tochange)->
-                       setText(utf8_to_wide(text).c_str());
+               ((gui::IGUIEditBox *)tochange)->setText(utf8_to_wide(text).c_str());
        }
        return false;
 }
@@ -2279,8 +2749,16 @@ GUIFormSpecMenu::ItemSpec GUIFormSpecMenu::getItemAtPos(v2s32 p) const
        for (const GUIFormSpecMenu::ListDrawSpec &s : m_inventorylists) {
                for(s32 i=0; i<s.geom.X*s.geom.Y; i++) {
                        s32 item_i = i + s.start_item_i;
-                       s32 x = (i%s.geom.X) * spacing.X;
-                       s32 y = (i/s.geom.X) * spacing.Y;
+
+                       s32 x;
+                       s32 y;
+                       if (s.real_coordinates) {
+                               x = (i%s.geom.X) * (imgsize.X * 1.25);
+                               y = (i/s.geom.X) * (imgsize.Y * 1.25);
+                       } else {
+                               x = (i%s.geom.X) * spacing.X;
+                               y = (i/s.geom.X) * spacing.Y;
+                       }
                        v2s32 p0(x,y);
                        core::rect<s32> rect = imgrect + s.pos + p0;
                        if(rect.isPointInside(p))
@@ -2293,7 +2771,7 @@ GUIFormSpecMenu::ItemSpec GUIFormSpecMenu::getItemAtPos(v2s32 p) const
        return ItemSpec(InventoryLocation(), "", -1);
 }
 
-void GUIFormSpecMenu::drawList(const ListDrawSpec &s, int phase,
+void GUIFormSpecMenu::drawList(const ListDrawSpec &s, int layer,
                bool &item_hovered)
 {
        video::IVideoDriver* driver = Environment->getVideoDriver();
@@ -2317,18 +2795,23 @@ void GUIFormSpecMenu::drawList(const ListDrawSpec &s, int phase,
 
        core::rect<s32> imgrect(0,0,imgsize.X,imgsize.Y);
 
-       for(s32 i=0; i<s.geom.X*s.geom.Y; i++)
-       {
+       for (s32 i = 0; i < s.geom.X * s.geom.Y; i++) {
                s32 item_i = i + s.start_item_i;
-               if(item_i >= (s32) ilist->getSize())
+               if (item_i >= (s32)ilist->getSize())
                        break;
-               s32 x = (i%s.geom.X) * spacing.X;
-               s32 y = (i/s.geom.X) * spacing.Y;
+
+               s32 x;
+               s32 y;
+               if (s.real_coordinates) {
+                       x = (i%s.geom.X) * (imgsize.X * 1.25);
+                       y = (i/s.geom.X) * (imgsize.Y * 1.25);
+               } else {
+                       x = (i%s.geom.X) * spacing.X;
+                       y = (i/s.geom.X) * spacing.Y;
+               }
                v2s32 p(x,y);
                core::rect<s32> rect = imgrect + s.pos + p;
-               ItemStack item;
-               if(ilist)
-                       item = ilist->getItem(item_i);
+               ItemStack item = ilist->getItem(item_i);
 
                bool selected = m_selected_item
                        && m_invmgr->getInventory(m_selected_item->inventoryloc) == inv
@@ -2338,7 +2821,7 @@ void GUIFormSpecMenu::drawList(const ListDrawSpec &s, int phase,
                ItemRotationKind rotation_kind = selected ? IT_ROT_SELECTED :
                        (hovering ? IT_ROT_HOVERED : IT_ROT_NONE);
 
-               if (phase == 0) {
+               if (layer == 0) {
                        if (hovering) {
                                item_hovered = true;
                                driver->draw2DRectangle(m_slotbg_h, rect, &AbsoluteClippingRect);
@@ -2368,41 +2851,24 @@ void GUIFormSpecMenu::drawList(const ListDrawSpec &s, int phase,
                                                                v2s32(x2 + border, y2)), NULL);
                }
 
-               if(phase == 1)
-               {
-                       // Draw item stack
-                       if(selected)
-                       {
+               if (layer == 1) {
+                       if (selected)
                                item.takeItem(m_selected_amount);
-                       }
-                       if(!item.empty())
-                       {
+
+                       if (!item.empty()) {
+                               // Draw item stack
                                drawItemStack(driver, m_font, item,
                                        rect, &AbsoluteClippingRect, m_client,
                                        rotation_kind);
-                       }
-
-                       // Draw tooltip
-                       std::wstring tooltip_text;
-                       if (hovering && !m_selected_item) {
-                               const std::string &desc = item.metadata.getString("description");
-                               if (desc.empty())
-                                       tooltip_text =
-                                               utf8_to_wide(item.getDefinition(m_client->idef()).description);
-                               else
-                                       tooltip_text = utf8_to_wide(desc);
-
-                               if (!item.name.empty()) {
-                                       if (tooltip_text.empty())
-                                               tooltip_text = utf8_to_wide(item.name);
+                               // Draw tooltip
+                               if (hovering && !m_selected_item) {
+                                       std::string tooltip = item.getDescription(m_client->idef());
                                        if (m_tooltip_append_itemname)
-                                               tooltip_text += utf8_to_wide(" [" + item.name + "]");
+                                               tooltip += "\n[" + item.name + "]";
+                                       showTooltip(utf8_to_wide(tooltip), m_default_tooltip_color,
+                                                       m_default_tooltip_bgcolor);
                                }
                        }
-                       if (!tooltip_text.empty()) {
-                               showTooltip(tooltip_text, m_default_tooltip_color,
-                                       m_default_tooltip_bgcolor);
-                       }
                }
        }
 }
@@ -2460,6 +2926,16 @@ void GUIFormSpecMenu::drawMenu()
 
        m_tooltip_element->setVisible(false);
 
+       for (const auto &pair : m_tooltip_rects) {
+               if (pair.first.isPointInside(m_pointer)) {
+                       const std::wstring &text = pair.second.tooltip;
+                       if (!text.empty()) {
+                               showTooltip(text, pair.second.color, pair.second.bgcolor);
+                               break;
+                       }
+               }
+       }
+
        /*
                Draw backgrounds
        */
@@ -2471,6 +2947,8 @@ void GUIFormSpecMenu::drawMenu()
                        core::rect<s32> imgrect(0, 0, spec.geom.X, spec.geom.Y);
                        // Image rectangle on screen
                        core::rect<s32> rect = imgrect + spec.pos;
+                       // Middle rect for 9-slicing
+                       core::rect<s32> middle = spec.middle;
 
                        if (spec.clip) {
                                core::dimension2d<s32> absrec_size = AbsoluteRect.getSize();
@@ -2480,12 +2958,23 @@ void GUIFormSpecMenu::drawMenu()
                                                                        AbsoluteRect.UpperLeftCorner.Y + absrec_size.Height + spec.pos.Y);
                        }
 
-                       const video::SColor color(255,255,255,255);
-                       const video::SColor colors[] = {color,color,color,color};
-                       draw2DImageFilterScaled(driver, texture, rect,
-                               core::rect<s32>(core::position2d<s32>(0,0),
-                                               core::dimension2di(texture->getOriginalSize())),
-                               NULL/*&AbsoluteClippingRect*/, colors, true);
+                       if (middle.getArea() == 0) {
+                               const video::SColor color(255, 255, 255, 255);
+                               const video::SColor colors[] = {color, color, color, color};
+                               draw2DImageFilterScaled(driver, texture, rect,
+                                               core::rect<s32>(core::position2d<s32>(0, 0),
+                                                               core::dimension2di(texture->getOriginalSize())),
+                                               NULL/*&AbsoluteClippingRect*/, colors, true);
+                       } else {
+                               // `-x` is interpreted as `w - x`
+                               if (middle.LowerRightCorner.X < 0) {
+                                       middle.LowerRightCorner.X += texture->getOriginalSize().Width;
+                               }
+                               if (middle.LowerRightCorner.Y < 0) {
+                                       middle.LowerRightCorner.Y += texture->getOriginalSize().Height;
+                               }
+                               draw2DImage9Slice(driver, texture, rect, middle);
+                       }
                } else {
                        errorstream << "GUIFormSpecMenu::drawMenu() Draw backgrounds unable to load texture:" << std::endl;
                        errorstream << "\t" << spec.name << std::endl;
@@ -2498,8 +2987,6 @@ void GUIFormSpecMenu::drawMenu()
        for (const GUIFormSpecMenu::BoxDrawSpec &spec : m_boxes) {
                irr::video::SColor todraw = spec.color;
 
-               todraw.setAlpha(140);
-
                core::rect<s32> rect(spec.pos.X,spec.pos.Y,
                                                        spec.pos.X + spec.geom.X,spec.pos.Y + spec.geom.Y);
 
@@ -2571,14 +3058,13 @@ void GUIFormSpecMenu::drawMenu()
 
        /*
                Draw items
-               Phase 0: Item slot rectangles
-               Phase 1: Item images; prepare tooltip
+               Layer 0: Item slot rectangles
+               Layer 1: Item images; prepare tooltip
        */
        bool item_hovered = false;
-       int start_phase = 0;
-       for (int phase = start_phase; phase <= 1; phase++) {
+       for (int layer = 0; layer < 2; layer++) {
                for (const GUIFormSpecMenu::ListDrawSpec &spec : m_inventorylists) {
-                       drawList(spec, phase, item_hovered);
+                       drawList(spec, layer, item_hovered);
                }
        }
        if (!item_hovered) {
@@ -2710,79 +3196,41 @@ void GUIFormSpecMenu::showTooltip(const std::wstring &text,
 
 void GUIFormSpecMenu::updateSelectedItem()
 {
-       // If the selected stack has become empty for some reason, deselect it.
-       // If the selected stack has become inaccessible, deselect it.
-       // If the selected stack has become smaller, adjust m_selected_amount.
-       ItemStack selected = verifySelectedItem();
-
-       // WARNING: BLACK MAGIC
-       // See if there is a stack suited for our current guess.
-       // If such stack does not exist, clear the guess.
-       if (!m_selected_content_guess.name.empty() &&
-                       selected.name == m_selected_content_guess.name &&
-                       selected.count == m_selected_content_guess.count){
-               // Selected item fits the guess. Skip the black magic.
-       } else if (!m_selected_content_guess.name.empty()) {
-               bool found = false;
-               for(u32 i=0; i<m_inventorylists.size() && !found; i++){
-                       const ListDrawSpec &s = m_inventorylists[i];
+       verifySelectedItem();
+
+       // If craftresult is nonempty and nothing else is selected, select it now.
+       if (!m_selected_item) {
+               for (const GUIFormSpecMenu::ListDrawSpec &s : m_inventorylists) {
+                       if (s.listname != "craftpreview")
+                               continue;
+
                        Inventory *inv = m_invmgr->getInventory(s.inventoryloc);
-                       if(!inv)
+                       if (!inv)
                                continue;
-                       InventoryList *list = inv->getList(s.listname);
-                       if(!list)
+
+                       InventoryList *list = inv->getList("craftresult");
+
+                       if (!list || list->getSize() == 0)
                                continue;
-                       for(s32 i=0; i<s.geom.X*s.geom.Y && !found; i++){
-                               u32 item_i = i + s.start_item_i;
-                               if(item_i >= list->getSize())
-                                       continue;
-                               ItemStack stack = list->getItem(item_i);
-                               if(stack.name == m_selected_content_guess.name &&
-                                               stack.count == m_selected_content_guess.count){
-                                       found = true;
-                                       infostream<<"Client: Changing selected content guess to "
-                                                       <<s.inventoryloc.dump()<<" "<<s.listname
-                                                       <<" "<<item_i<<std::endl;
-                                       delete m_selected_item;
-                                       m_selected_item = new ItemSpec(s.inventoryloc, s.listname, item_i);
-                                       m_selected_amount = stack.count;
-                               }
-                       }
-               }
-               if(!found){
-                       infostream<<"Client: Discarding selected content guess: "
-                                       <<m_selected_content_guess.getItemString()<<std::endl;
-                       m_selected_content_guess.name = "";
-               }
-       }
 
-       // If craftresult is nonempty and nothing else is selected, select it now.
-       if(!m_selected_item)
-       {
-               for (const GUIFormSpecMenu::ListDrawSpec &s : m_inventorylists) {
-                       if(s.listname == "craftpreview")
-                       {
-                               Inventory *inv = m_invmgr->getInventory(s.inventoryloc);
-                               InventoryList *list = inv->getList("craftresult");
-                               if(list && list->getSize() >= 1 && !list->getItem(0).empty())
-                               {
-                                       m_selected_item = new ItemSpec;
-                                       m_selected_item->inventoryloc = s.inventoryloc;
-                                       m_selected_item->listname = "craftresult";
-                                       m_selected_item->i = 0;
-                                       m_selected_amount = 0;
-                                       m_selected_dragging = false;
-                                       break;
-                               }
-                       }
+                       const ItemStack &item = list->getItem(0);
+                       if (item.empty())
+                               continue;
+
+                       // Grab selected item from the crafting result list
+                       m_selected_item = new ItemSpec;
+                       m_selected_item->inventoryloc = s.inventoryloc;
+                       m_selected_item->listname = "craftresult";
+                       m_selected_item->i = 0;
+                       m_selected_amount = item.count;
+                       m_selected_dragging = false;
+                       break;
                }
        }
 
        // If craftresult is selected, keep the whole stack selected
-       if(m_selected_item && m_selected_item->listname == "craftresult")
-       {
+       if (m_selected_item && m_selected_item->listname == "craftresult")
                m_selected_amount = verifySelectedItem().count;
-       }
 }
 
 ItemStack GUIFormSpecMenu::verifySelectedItem()
@@ -2803,9 +3251,15 @@ ItemStack GUIFormSpecMenu::verifySelectedItem()
                                if(list && (u32) m_selected_item->i < list->getSize())
                                {
                                        ItemStack stack = list->getItem(m_selected_item->i);
-                                       if(m_selected_amount > stack.count)
-                                               m_selected_amount = stack.count;
-                                       if(!stack.empty())
+                                       if (!m_selected_swap.empty()) {
+                                               if (m_selected_swap.name == stack.name &&
+                                                               m_selected_swap.count == stack.count)
+                                                       m_selected_swap.clear();
+                                       } else {
+                                               m_selected_amount = std::min(m_selected_amount, stack.count);
+                                       }
+
+                                       if (!stack.empty())
                                                return stack;
                                }
                        }
@@ -2999,7 +3453,9 @@ bool GUIFormSpecMenu::preprocessEvent(const SEvent& event)
                        gui::IGUIElement *focused = Environment->getFocus();
                        if (focused && isMyChild(focused) &&
                                        (focused->getType() == gui::EGUIET_LIST_BOX ||
-                                        focused->getType() == gui::EGUIET_CHECK_BOX)) {
+                                       focused->getType() == gui::EGUIET_CHECK_BOX) &&
+                                       (focused->getParent()->getType() != gui::EGUIET_COMBO_BOX ||
+                                       event.KeyInput.Key != KEY_RETURN)) {
                                OnEvent(event);
                                return true;
                        }
@@ -3035,156 +3491,6 @@ bool GUIFormSpecMenu::preprocessEvent(const SEvent& event)
                }
        }
 
-       #ifdef __ANDROID__
-       // display software keyboard when clicking edit boxes
-       if (event.EventType == EET_MOUSE_INPUT_EVENT
-                       && event.MouseInput.Event == EMIE_LMOUSE_PRESSED_DOWN) {
-               gui::IGUIElement *hovered =
-                       Environment->getRootGUIElement()->getElementFromPoint(
-                               core::position2d<s32>(event.MouseInput.X, event.MouseInput.Y));
-               if ((hovered) && (hovered->getType() == irr::gui::EGUIET_EDIT_BOX)) {
-                       bool retval = hovered->OnEvent(event);
-                       if (retval) {
-                               Environment->setFocus(hovered);
-                       }
-                       m_JavaDialogFieldName = getNameByID(hovered->getID());
-                       std::string message   = gettext("Enter ");
-                       std::string label     = wide_to_utf8(getLabelByID(hovered->getID()));
-                       if (label == "") {
-                               label = "text";
-                       }
-                       message += gettext(label) + ":";
-
-                       /* single line text input */
-                       int type = 2;
-
-                       /* multi line text input */
-                       if (((gui::IGUIEditBox*) hovered)->isMultiLineEnabled()) {
-                               type = 1;
-                       }
-
-                       /* passwords are always single line */
-                       if (((gui::IGUIEditBox*) hovered)->isPasswordBox()) {
-                               type = 3;
-                       }
-
-                       porting::showInputDialog(gettext("ok"), "",
-                                       wide_to_utf8(((gui::IGUIEditBox*) hovered)->getText()),
-                                       type);
-                       return retval;
-               }
-       }
-
-       if (event.EventType == EET_TOUCH_INPUT_EVENT)
-       {
-               SEvent translated;
-               memset(&translated, 0, sizeof(SEvent));
-               translated.EventType   = EET_MOUSE_INPUT_EVENT;
-               gui::IGUIElement* root = Environment->getRootGUIElement();
-
-               if (!root) {
-                       errorstream
-                       << "GUIFormSpecMenu::preprocessEvent unable to get root element"
-                       << std::endl;
-                       return false;
-               }
-               gui::IGUIElement* hovered = root->getElementFromPoint(
-                       core::position2d<s32>(
-                                       event.TouchInput.X,
-                                       event.TouchInput.Y));
-
-               translated.MouseInput.X = event.TouchInput.X;
-               translated.MouseInput.Y = event.TouchInput.Y;
-               translated.MouseInput.Control = false;
-
-               bool dont_send_event = false;
-
-               if (event.TouchInput.touchedCount == 1) {
-                       switch (event.TouchInput.Event) {
-                               case ETIE_PRESSED_DOWN:
-                                       m_pointer = v2s32(event.TouchInput.X,event.TouchInput.Y);
-                                       translated.MouseInput.Event = EMIE_LMOUSE_PRESSED_DOWN;
-                                       translated.MouseInput.ButtonStates = EMBSM_LEFT;
-                                       m_down_pos = m_pointer;
-                                       break;
-                               case ETIE_MOVED:
-                                       m_pointer = v2s32(event.TouchInput.X,event.TouchInput.Y);
-                                       translated.MouseInput.Event = EMIE_MOUSE_MOVED;
-                                       translated.MouseInput.ButtonStates = EMBSM_LEFT;
-                                       break;
-                               case ETIE_LEFT_UP:
-                                       translated.MouseInput.Event = EMIE_LMOUSE_LEFT_UP;
-                                       translated.MouseInput.ButtonStates = 0;
-                                       hovered = root->getElementFromPoint(m_down_pos);
-                                       /* we don't have a valid pointer element use last
-                                        * known pointer pos */
-                                       translated.MouseInput.X = m_pointer.X;
-                                       translated.MouseInput.Y = m_pointer.Y;
-
-                                       /* reset down pos */
-                                       m_down_pos = v2s32(0,0);
-                                       break;
-                               default:
-                                       dont_send_event = true;
-                                       //this is not supposed to happen
-                                       errorstream
-                                       << "GUIFormSpecMenu::preprocessEvent unexpected usecase Event="
-                                       << event.TouchInput.Event << std::endl;
-                       }
-               } else if ( (event.TouchInput.touchedCount == 2) &&
-                               (event.TouchInput.Event == ETIE_PRESSED_DOWN) ) {
-                       hovered = root->getElementFromPoint(m_down_pos);
-
-                       translated.MouseInput.Event = EMIE_RMOUSE_PRESSED_DOWN;
-                       translated.MouseInput.ButtonStates = EMBSM_LEFT | EMBSM_RIGHT;
-                       translated.MouseInput.X = m_pointer.X;
-                       translated.MouseInput.Y = m_pointer.Y;
-
-                       if (hovered) {
-                               hovered->OnEvent(translated);
-                       }
-
-                       translated.MouseInput.Event = EMIE_RMOUSE_LEFT_UP;
-                       translated.MouseInput.ButtonStates = EMBSM_LEFT;
-
-
-                       if (hovered) {
-                               hovered->OnEvent(translated);
-                       }
-                       dont_send_event = true;
-               }
-               /* ignore unhandled 2 touch events ... accidental moving for example */
-               else if (event.TouchInput.touchedCount == 2) {
-                       dont_send_event = true;
-               }
-               else if (event.TouchInput.touchedCount > 2) {
-                       errorstream
-                       << "GUIFormSpecMenu::preprocessEvent to many multitouch events "
-                       << event.TouchInput.touchedCount << " ignoring them" << std::endl;
-               }
-
-               if (dont_send_event) {
-                       return true;
-               }
-
-               /* check if translated event needs to be preprocessed again */
-               if (preprocessEvent(translated)) {
-                       return true;
-               }
-               if (hovered) {
-                       grab();
-                       bool retval = hovered->OnEvent(translated);
-
-                       if (event.TouchInput.Event == ETIE_LEFT_UP) {
-                               /* reset pointer */
-                               m_pointer = v2s32(0,0);
-                       }
-                       drop();
-                       return retval;
-               }
-       }
-       #endif
-
        if (event.EventType == irr::EET_JOYSTICK_INPUT_EVENT) {
                /* TODO add a check like:
                if (event.JoystickEvent != joystick_we_listen_for)
@@ -3204,7 +3510,7 @@ bool GUIFormSpecMenu::preprocessEvent(const SEvent& event)
                return handled;
        }
 
-       return false;
+       return GUIModalMenu::preprocessEvent(event);
 }
 
 /******************************************************************************/
@@ -3276,6 +3582,19 @@ void GUIFormSpecMenu::tryClose()
        }
 }
 
+enum ButtonEventType : u8
+{
+       BET_LEFT,
+       BET_RIGHT,
+       BET_MIDDLE,
+       BET_WHEEL_UP,
+       BET_WHEEL_DOWN,
+       BET_UP,
+       BET_DOWN,
+       BET_MOVE,
+       BET_OTHER
+};
+
 bool GUIFormSpecMenu::OnEvent(const SEvent& event)
 {
        if (event.EventType==EET_KEY_INPUT_EVENT) {
@@ -3381,29 +3700,43 @@ bool GUIFormSpecMenu::OnEvent(const SEvent& event)
                        s_count = list_s->getItem(s.i).count;
                } while(0);
 
-               bool identical = (m_selected_item != NULL) && s.isValid() &&
+               bool identical = m_selected_item && s.isValid() &&
                        (inv_selected == inv_s) &&
                        (m_selected_item->listname == s.listname) &&
                        (m_selected_item->i == s.i);
 
-               // buttons: 0 = left, 1 = right, 2 = middle
-               // up/down: 0 = down (press), 1 = up (release), 2 = unknown event, -1 movement
-               int button = 0;
-               int updown = 2;
-               if (event.MouseInput.Event == EMIE_LMOUSE_PRESSED_DOWN)
-                       { button = 0; updown = 0; }
-               else if (event.MouseInput.Event == EMIE_RMOUSE_PRESSED_DOWN)
-                       { button = 1; updown = 0; }
-               else if (event.MouseInput.Event == EMIE_MMOUSE_PRESSED_DOWN)
-                       { button = 2; updown = 0; }
-               else if (event.MouseInput.Event == EMIE_LMOUSE_LEFT_UP)
-                       { button = 0; updown = 1; }
-               else if (event.MouseInput.Event == EMIE_RMOUSE_LEFT_UP)
-                       { button = 1; updown = 1; }
-               else if (event.MouseInput.Event == EMIE_MMOUSE_LEFT_UP)
-                       { button = 2; updown = 1; }
-               else if (event.MouseInput.Event == EMIE_MOUSE_MOVED)
-                       { updown = -1;}
+               ButtonEventType button = BET_LEFT;
+               ButtonEventType updown = BET_OTHER;
+               switch (event.MouseInput.Event) {
+               case EMIE_LMOUSE_PRESSED_DOWN:
+                       button = BET_LEFT; updown = BET_DOWN;
+                       break;
+               case EMIE_RMOUSE_PRESSED_DOWN:
+                       button = BET_RIGHT; updown = BET_DOWN;
+                       break;
+               case EMIE_MMOUSE_PRESSED_DOWN:
+                       button = BET_MIDDLE; updown = BET_DOWN;
+                       break;
+               case EMIE_MOUSE_WHEEL:
+                       button = (event.MouseInput.Wheel > 0) ?
+                               BET_WHEEL_UP : BET_WHEEL_DOWN;
+                       updown = BET_DOWN;
+                       break;
+               case EMIE_LMOUSE_LEFT_UP:
+                       button = BET_LEFT; updown = BET_UP;
+                       break;
+               case EMIE_RMOUSE_LEFT_UP:
+                       button = BET_RIGHT; updown = BET_UP;
+                       break;
+               case EMIE_MMOUSE_LEFT_UP:
+                       button = BET_MIDDLE; updown = BET_UP;
+                       break;
+               case EMIE_MOUSE_MOVED:
+                       updown = BET_MOVE;
+                       break;
+               default:
+                       break;
+               }
 
                // Set this number to a positive value to generate a move action
                // from m_selected_item to s.
@@ -3420,7 +3753,8 @@ bool GUIFormSpecMenu::OnEvent(const SEvent& event)
                // Set this number to a positive value to generate a craft action at s.
                u32 craft_amount = 0;
 
-               if (updown == 0) {
+               switch (updown) {
+               case BET_DOWN:
                        // Some mouse button has been pressed
 
                        //infostream<<"Mouse button "<<button<<" pressed at p=("
@@ -3430,31 +3764,30 @@ bool GUIFormSpecMenu::OnEvent(const SEvent& event)
 
                        if (s.isValid() && s.listname == "craftpreview") {
                                // Craft preview has been clicked: craft
-                               craft_amount = (button == 2 ? 10 : 1);
-                       } else if (m_selected_item == NULL) {
-                               if (s_count != 0) {
+                               craft_amount = (button == BET_MIDDLE ? 10 : 1);
+                       } else if (!m_selected_item) {
+                               if (s_count && button != BET_WHEEL_UP) {
                                        // Non-empty stack has been clicked: select or shift-move it
                                        m_selected_item = new ItemSpec(s);
 
                                        u32 count;
-                                       if (button == 1)  // right
+                                       if (button == BET_RIGHT)
                                                count = (s_count + 1) / 2;
-                                       else if (button == 2)  // middle
+                                       else if (button == BET_MIDDLE)
                                                count = MYMIN(s_count, 10);
+                                       else if (button == BET_WHEEL_DOWN)
+                                               count = 1;
                                        else  // left
                                                count = s_count;
 
                                        if (!event.MouseInput.Shift) {
                                                // no shift: select item
                                                m_selected_amount = count;
-                                               m_selected_dragging = true;
+                                               m_selected_dragging = button != BET_WHEEL_DOWN;
                                                m_auto_place = false;
                                        } else {
-                                               // shift pressed: move item
-                                               if (button != 1)
-                                                       shift_move_amount = count;
-                                               else // count of 1 at left click like after drag & drop
-                                                       shift_move_amount = 1;
+                                               // shift pressed: move item, right click moves 1
+                                               shift_move_amount = button == BET_RIGHT ? 1 : count;
                                        }
                                }
                        } else { // m_selected_item != NULL
@@ -3462,47 +3795,54 @@ bool GUIFormSpecMenu::OnEvent(const SEvent& event)
 
                                if (s.isValid()) {
                                        // Clicked a slot: move
-                                       if (button == 1)  // right
+                                       if (button == BET_RIGHT || button == BET_WHEEL_UP)
                                                move_amount = 1;
-                                       else if (button == 2)  // middle
+                                       else if (button == BET_MIDDLE)
                                                move_amount = MYMIN(m_selected_amount, 10);
-                                       else  // left
+                                       else if (button == BET_LEFT)
                                                move_amount = m_selected_amount;
+                                       // else wheeldown
 
                                        if (identical) {
-                                               if (move_amount >= m_selected_amount)
-                                                       m_selected_amount = 0;
-                                               else
-                                                       m_selected_amount -= move_amount;
-                                               move_amount = 0;
+                                               if (button == BET_WHEEL_DOWN) {
+                                                       if (m_selected_amount < s_count)
+                                                               ++m_selected_amount;
+                                               } else {
+                                                       if (move_amount >= m_selected_amount)
+                                                               m_selected_amount = 0;
+                                                       else
+                                                               m_selected_amount -= move_amount;
+                                                       move_amount = 0;
+                                               }
                                        }
-                               }
-                               else if (!getAbsoluteClippingRect().isPointInside(m_pointer)) {
+                               } else if (!getAbsoluteClippingRect().isPointInside(m_pointer)
+                                               && button != BET_WHEEL_DOWN) {
                                        // Clicked outside of the window: drop
-                                       if (button == 1)  // right
+                                       if (button == BET_RIGHT || button == BET_WHEEL_UP)
                                                drop_amount = 1;
-                                       else if (button == 2)  // middle
+                                       else if (button == BET_MIDDLE)
                                                drop_amount = MYMIN(m_selected_amount, 10);
                                        else  // left
                                                drop_amount = m_selected_amount;
                                }
                        }
-               }
-               else if (updown == 1) {
+               break;
+               case BET_UP:
                        // Some mouse button has been released
 
                        //infostream<<"Mouse button "<<button<<" released at p=("
                        //      <<p.X<<","<<p.Y<<")"<<std::endl;
 
-                       if (m_selected_item != NULL && m_selected_dragging && s.isValid()) {
-                               if (!identical) {
-                                       // Dragged to different slot: move all selected
-                                       move_amount = m_selected_amount;
+                       if (m_selected_dragging && m_selected_item) {
+                               if (s.isValid()) {
+                                       if (!identical) {
+                                               // Dragged to different slot: move all selected
+                                               move_amount = m_selected_amount;
+                                       }
+                               } else if (!getAbsoluteClippingRect().isPointInside(m_pointer)) {
+                                       // Dragged outside of window: drop all selected
+                                       drop_amount = m_selected_amount;
                                }
-                       } else if (m_selected_item != NULL && m_selected_dragging &&
-                                       !(getAbsoluteClippingRect().isPointInside(m_pointer))) {
-                               // Dragged outside of window: drop all selected
-                               drop_amount = m_selected_amount;
                        }
 
                        m_selected_dragging = false;
@@ -3511,11 +3851,12 @@ bool GUIFormSpecMenu::OnEvent(const SEvent& event)
                        // + click changes to drop item when moved mode
                        if (m_selected_item)
                                m_auto_place = true;
-               } else if (updown == -1) {
+               break;
+               case BET_MOVE:
                        // Mouse has been moved and rmb is down and mouse pointer just
                        // entered a new inventory field (checked in the entry-if, this
                        // is the only action here that is generated by mouse movement)
-                       if (m_selected_item != NULL && s.isValid()) {
+                       if (m_selected_item && s.isValid() && s.listname != "craftpreview") {
                                // Move 1 item
                                // TODO: middle mouse to move 10 items might be handy
                                if (m_auto_place) {
@@ -3531,6 +3872,9 @@ bool GUIFormSpecMenu::OnEvent(const SEvent& event)
                                                move_amount = 1;
                                }
                        }
+               break;
+               default:
+                       break;
                }
 
                // Possibly send inventory action to server
@@ -3550,38 +3894,45 @@ bool GUIFormSpecMenu::OnEvent(const SEvent& event)
                        // Check how many items can be moved
                        move_amount = stack_from.count = MYMIN(move_amount, stack_from.count);
                        ItemStack leftover = stack_to.addItem(stack_from, m_client->idef());
+                       bool move = true;
                        // If source stack cannot be added to destination stack at all,
                        // they are swapped
-                       if ((leftover.count == stack_from.count) &&
-                                       (leftover.name == stack_from.name)) {
-                               m_selected_amount = stack_to.count;
-                               // In case the server doesn't directly swap them but instead
-                               // moves stack_to somewhere else, set this
-                               m_selected_content_guess = stack_to;
-                               m_selected_content_guess_inventory = s.inventoryloc;
+                       if (leftover.count == stack_from.count &&
+                                       leftover.name == stack_from.name) {
+
+                               if (m_selected_swap.empty()) {
+                                       m_selected_amount = stack_to.count;
+                                       m_selected_dragging = false;
+
+                                       // WARNING: BLACK MAGIC, BUT IN A REDUCED SET
+                                       // Skip next validation checks due async inventory calls
+                                       m_selected_swap = stack_to;
+                               } else {
+                                       move = false;
+                               }
                        }
                        // Source stack goes fully into destination stack
                        else if (leftover.empty()) {
                                m_selected_amount -= move_amount;
-                               m_selected_content_guess = ItemStack(); // Clear
                        }
                        // Source stack goes partly into destination stack
                        else {
                                move_amount -= leftover.count;
                                m_selected_amount -= move_amount;
-                               m_selected_content_guess = ItemStack(); // Clear
                        }
 
-                       infostream << "Handing IAction::Move to manager" << std::endl;
-                       IMoveAction *a = new IMoveAction();
-                       a->count = move_amount;
-                       a->from_inv = m_selected_item->inventoryloc;
-                       a->from_list = m_selected_item->listname;
-                       a->from_i = m_selected_item->i;
-                       a->to_inv = s.inventoryloc;
-                       a->to_list = s.listname;
-                       a->to_i = s.i;
-                       m_invmgr->inventoryAction(a);
+                       if (move) {
+                               infostream << "Handing IAction::Move to manager" << std::endl;
+                               IMoveAction *a = new IMoveAction();
+                               a->count = move_amount;
+                               a->from_inv = m_selected_item->inventoryloc;
+                               a->from_list = m_selected_item->listname;
+                               a->from_i = m_selected_item->i;
+                               a->to_inv = s.inventoryloc;
+                               a->to_list = s.listname;
+                               a->to_i = s.i;
+                               m_invmgr->inventoryAction(a);
+                       }
                } else if (shift_move_amount > 0) {
                        u32 mis = m_inventory_rings.size();
                        u32 i = 0;
@@ -3607,45 +3958,19 @@ bool GUIFormSpecMenu::OnEvent(const SEvent& event)
                                        break;
                                ItemStack stack_from = list_from->getItem(s.i);
                                assert(shift_move_amount <= stack_from.count);
-                               if (m_client->getProtoVersion() >= 25) {
-                                       infostream << "Handing IAction::Move to manager" << std::endl;
-                                       IMoveAction *a = new IMoveAction();
-                                       a->count = shift_move_amount;
-                                       a->from_inv = s.inventoryloc;
-                                       a->from_list = s.listname;
-                                       a->from_i = s.i;
-                                       a->to_inv = to_inv_sp.inventoryloc;
-                                       a->to_list = to_inv_sp.listname;
-                                       a->move_somewhere = true;
-                                       m_invmgr->inventoryAction(a);
-                               } else {
-                                       // find a place (or more than one) to add the new item
-                                       u32 ilt_size = list_to->getSize();
-                                       ItemStack leftover;
-                                       for (u32 slot_to = 0; slot_to < ilt_size
-                                                       && shift_move_amount > 0; slot_to++) {
-                                               list_to->itemFits(slot_to, stack_from, &leftover);
-                                               if (leftover.count < stack_from.count) {
-                                                       infostream << "Handing IAction::Move to manager" << std::endl;
-                                                       IMoveAction *a = new IMoveAction();
-                                                       a->count = MYMIN(shift_move_amount,
-                                                               (u32) (stack_from.count - leftover.count));
-                                                       shift_move_amount -= a->count;
-                                                       a->from_inv = s.inventoryloc;
-                                                       a->from_list = s.listname;
-                                                       a->from_i = s.i;
-                                                       a->to_inv = to_inv_sp.inventoryloc;
-                                                       a->to_list = to_inv_sp.listname;
-                                                       a->to_i = slot_to;
-                                                       m_invmgr->inventoryAction(a);
-                                                       stack_from = leftover;
-                                               }
-                                       }
-                               }
+
+                               infostream << "Handing IAction::Move to manager" << std::endl;
+                               IMoveAction *a = new IMoveAction();
+                               a->count = shift_move_amount;
+                               a->from_inv = s.inventoryloc;
+                               a->from_list = s.listname;
+                               a->from_i = s.i;
+                               a->to_inv = to_inv_sp.inventoryloc;
+                               a->to_list = to_inv_sp.listname;
+                               a->move_somewhere = true;
+                               m_invmgr->inventoryAction(a);
                        } while (0);
                } else if (drop_amount > 0) {
-                       m_selected_content_guess = ItemStack(); // Clear
-
                        // Send IAction::Drop
 
                        assert(m_selected_item && m_selected_item->isValid());
@@ -3674,8 +3999,6 @@ bool GUIFormSpecMenu::OnEvent(const SEvent& event)
                        if (m_selected_item == NULL ||
                                        !m_selected_item->isValid() || m_selected_item->listname == "craftresult") {
 
-                               m_selected_content_guess = ItemStack(); // Clear
-
                                assert(inv_s);
 
                                // Send IACTION_CRAFT
@@ -3689,11 +4012,11 @@ bool GUIFormSpecMenu::OnEvent(const SEvent& event)
 
                // If m_selected_amount has been decreased to zero, deselect
                if (m_selected_amount == 0) {
+                       m_selected_swap.clear();
                        delete m_selected_item;
                        m_selected_item = NULL;
                        m_selected_amount = 0;
                        m_selected_dragging = false;
-                       m_selected_content_guess = ItemStack();
                }
                m_old_pointer = m_pointer;
        }
@@ -3867,3 +4190,27 @@ std::wstring GUIFormSpecMenu::getLabelByID(s32 id)
        }
        return L"";
 }
+
+StyleSpec GUIFormSpecMenu::getStyleForElement(const std::string &type,
+               const std::string &name, const std::string &parent_type) {
+       StyleSpec ret;
+
+       if (!parent_type.empty()) {
+               auto it = theme_by_type.find(parent_type);
+               if (it != theme_by_type.end()) {
+                       ret |= it->second;
+               }
+       }
+
+       auto it = theme_by_type.find(type);
+       if (it != theme_by_type.end()) {
+               ret |= it->second;
+       }
+
+       it = theme_by_name.find(name);
+       if (it != theme_by_name.end()) {
+               ret |= it->second;
+       }
+
+       return ret;
+}