diff --git a/data/resource-default/default.ui b/data/resource-default/default.ui
new file mode 100644
index 0000000000..08991246e6
--- /dev/null
+++ b/data/resource-default/default.ui
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/drape_frontend/drape_frontend.pro b/drape_frontend/drape_frontend.pro
index a0183c5e5a..04d373ab1f 100644
--- a/drape_frontend/drape_frontend.pro
+++ b/drape_frontend/drape_frontend.pro
@@ -40,6 +40,7 @@ SOURCES += \
map_data_provider.cpp \
user_mark_shapes.cpp \
user_marks_provider.cpp \
+ gui_skin.cpp \
HEADERS += \
engine_context.hpp \
@@ -78,3 +79,4 @@ HEADERS += \
map_data_provider.hpp \
user_mark_shapes.hpp \
user_marks_provider.hpp \
+ gui_skin.hpp \
diff --git a/drape_frontend/drape_frontend_tests/drape_frontend_tests.pro b/drape_frontend/drape_frontend_tests/drape_frontend_tests.pro
index 980cee98bf..ea7c8fd2dc 100644
--- a/drape_frontend/drape_frontend_tests/drape_frontend_tests.pro
+++ b/drape_frontend/drape_frontend_tests/drape_frontend_tests.pro
@@ -4,12 +4,18 @@ CONFIG += console warn_on
CONFIG -= app_bundle
TEMPLATE = app
-DEPENDENCIES = drape_frontend drape base fribidi
+DEPENDENCIES = drape_frontend coding platform drape base fribidi expat
ROOT_DIR = ../..
include($$ROOT_DIR/common.pri)
+macx-* {
+ LIBS *= "-framework CoreLocation" "-framework Foundation" "-framework CoreWLAN" \
+ "-framework QuartzCore" "-framework IOKit"
+}
+
SOURCES += \
../../testing/testingmain.cpp \
memory_feature_index_tests.cpp \
fribidi_tests.cpp \
object_pool_tests.cpp \
+ gui_skin_tests.cpp \
diff --git a/drape_frontend/drape_frontend_tests/gui_skin_tests.cpp b/drape_frontend/drape_frontend_tests/gui_skin_tests.cpp
new file mode 100644
index 0000000000..13292aeaf6
--- /dev/null
+++ b/drape_frontend/drape_frontend_tests/gui_skin_tests.cpp
@@ -0,0 +1,60 @@
+#include "../../base/SRC_FIRST.hpp"
+#include "../../testing/testing.hpp"
+
+#include "../gui_skin.hpp"
+#include "../visual_params.hpp"
+
+UNIT_TEST(ParseDefaultSkinTest)
+{
+ df::VisualParams::Init(2.0f, 1024);
+ df::GuiSkin skin(df::ResolveGuiSkinFile("default"));
+ float width = 600.0f;
+ float height = 800.0f;
+ skin.Resize(width, height);
+
+ df::GuiPosition compassPos = skin.ResolvePosition(df::GuiSkin::GuiElement::Compass);
+ TEST_EQUAL(compassPos.m_anchor, dp::Center, ());
+ TEST_ALMOST_EQUAL(compassPos.m_pixelPivot.x, 27.0f * 2.0f, ());
+ TEST_ALMOST_EQUAL(compassPos.m_pixelPivot.y, height - 57.0f * 2.0f, ());
+
+ df::GuiPosition rulerPos = skin.ResolvePosition(df::GuiSkin::GuiElement::Ruler);
+ TEST_EQUAL(rulerPos.m_anchor, dp::Right, ());
+ TEST_ALMOST_EQUAL(rulerPos.m_pixelPivot.x, width - 6.0f * 2.0f, ());
+ TEST_ALMOST_EQUAL(rulerPos.m_pixelPivot.y, height - 42.0f * 2.0f, ());
+
+ df::GuiPosition copyRightPos = skin.ResolvePosition(df::GuiSkin::GuiElement::Copyright);
+ TEST_EQUAL(copyRightPos.m_anchor, dp::Right, ());
+ TEST_ALMOST_EQUAL(copyRightPos.m_pixelPivot.x, width - 6.0f * 2.0f, ());
+ TEST_ALMOST_EQUAL(copyRightPos.m_pixelPivot.y, height - 42.0f * 2.0f, ());
+
+ df::GuiPosition countryStatusPos = skin.ResolvePosition(df::GuiSkin::GuiElement::CountryStatus);
+ TEST_EQUAL(countryStatusPos.m_anchor, dp::Center, ());
+ TEST_ALMOST_EQUAL(countryStatusPos.m_pixelPivot.x, width / 2.0f, ());
+ TEST_ALMOST_EQUAL(countryStatusPos.m_pixelPivot.y, height / 2.0f, ());
+
+ {
+ width = 800.0f;
+ height = 600.0f;
+ skin.Resize(width, height);
+
+ df::GuiPosition compassPos = skin.ResolvePosition(df::GuiSkin::GuiElement::Compass);
+ TEST_EQUAL(compassPos.m_anchor, dp::Center, ());
+ TEST_ALMOST_EQUAL(compassPos.m_pixelPivot.x, 18.0f * 2.0f, ());
+ TEST_ALMOST_EQUAL(compassPos.m_pixelPivot.y, height - 11.4f * 2.0f, ());
+
+ df::GuiPosition rulerPos = skin.ResolvePosition(df::GuiSkin::GuiElement::Ruler);
+ TEST_EQUAL(rulerPos.m_anchor, dp::Right, ());
+ TEST_ALMOST_EQUAL(rulerPos.m_pixelPivot.x, width - 70.4f * 2.0f, ());
+ TEST_ALMOST_EQUAL(rulerPos.m_pixelPivot.y, height - 10.5f * 2.0f, ());
+
+ df::GuiPosition copyRightPos = skin.ResolvePosition(df::GuiSkin::GuiElement::Copyright);
+ TEST_EQUAL(copyRightPos.m_anchor, dp::Right, ());
+ TEST_ALMOST_EQUAL(copyRightPos.m_pixelPivot.x, width - 70.4f * 2.0f, ());
+ TEST_ALMOST_EQUAL(copyRightPos.m_pixelPivot.y, height - 10.5f * 2.0f, ());
+
+ df::GuiPosition countryStatusPos = skin.ResolvePosition(df::GuiSkin::GuiElement::CountryStatus);
+ TEST_EQUAL(countryStatusPos.m_anchor, dp::Center, ());
+ TEST_ALMOST_EQUAL(countryStatusPos.m_pixelPivot.x, width / 2.0f, ());
+ TEST_ALMOST_EQUAL(countryStatusPos.m_pixelPivot.y, height / 2.0f, ());
+ }
+}
diff --git a/drape_frontend/gui_skin.cpp b/drape_frontend/gui_skin.cpp
new file mode 100644
index 0000000000..085ebf72a6
--- /dev/null
+++ b/drape_frontend/gui_skin.cpp
@@ -0,0 +1,320 @@
+#include "gui_skin.hpp"
+#include "visual_params.hpp"
+
+#include "../base/string_utils.hpp"
+#include "../coding/parse_xml.hpp"
+#include "../platform/platform.hpp"
+
+#include "../std/function.hpp"
+
+namespace df
+{
+
+namespace
+{
+
+bool IsSimple(dp::Anchor anchor)
+{
+ return anchor >= 0 && anchor <= 8;
+}
+
+bool IsAnchor(dp::Anchor anchor)
+{
+ return anchor >= 0 && anchor <= 10;
+}
+
+dp::Anchor ParseValueAnchor(string const & value)
+{
+ if (value == "center")
+ return dp::Center;
+ else if (value == "left")
+ return dp::Left;
+ else if (value == "right")
+ return dp::Right;
+ else if (value == "top")
+ return dp::Top;
+ else if (value == "bottom")
+ return dp::Bottom;
+ else
+ ASSERT(false, ());
+
+ return dp::Center;
+}
+
+dp::Anchor MergeAnchors(dp::Anchor src, dp::Anchor dst)
+{
+ ASSERT(IsSimple(dst), ());
+ ASSERT(IsSimple(src), ());
+ dp::Anchor result = static_cast(src | dst);
+ ASSERT(IsAnchor(result), ());
+ return result;
+}
+
+float ParseFloat(string const & v)
+{
+ double d = 0.0f;
+ VERIFY(strings::to_double(v, d), ());
+ return d;
+}
+
+class ResolverParser
+{
+public:
+ enum class Element
+ {
+ Empty,
+ Anchor,
+ Relative,
+ Offset
+ };
+
+ ResolverParser()
+ : m_element(Element::Empty) {}
+
+ void Parse(string const & attr, string const & value)
+ {
+ ASSERT(m_element != Element::Empty, ());
+
+ if (attr == "x")
+ {
+ ASSERT(m_element == Element::Offset, ());
+ m_resolver.SetOffsetX(ParseFloat(value));
+ }
+ else if (attr == "y")
+ {
+ ASSERT(m_element == Element::Offset, ());
+ m_resolver.SetOffsetY(ParseFloat(value));
+ }
+ else if (attr == "vertical")
+ {
+ if (m_element == Element::Anchor)
+ m_resolver.SetAnchorVertical(ParseValueAnchor(value));
+ else if (m_element == Element::Relative)
+ m_resolver.SetRelativeVertical(ParseValueAnchor(value));
+ else
+ ASSERT(false, ());
+ }
+ else if (attr == "horizontal")
+ {
+ if (m_element == Element::Anchor)
+ m_resolver.SetAnchorHorizontal(ParseValueAnchor(value));
+ else if (m_element == Element::Relative)
+ m_resolver.SetRelativeHorizontal(ParseValueAnchor(value));
+ else
+ ASSERT(false, ());
+ }
+ }
+
+ void Reset()
+ {
+ m_resolver = PositionResolver();
+ }
+
+ PositionResolver const & GetResolver() const
+ {
+ return m_resolver;
+ }
+
+ void SetElement(Element e)
+ {
+ m_element = e;
+ }
+
+private:
+ Element m_element;
+ PositionResolver m_resolver;
+};
+
+class SkinLoader
+{
+public:
+ SkinLoader(map > & skin)
+ : m_skin(skin) {}
+
+ bool Push(string const & element)
+ {
+ if (m_inElement == false)
+ {
+ if (element == "root")
+ return true;
+
+ m_inElement = true;
+ if (element == "ruler")
+ m_currentElement = GuiSkin::GuiElement::Ruler;
+ else if (element == "compass")
+ m_currentElement = GuiSkin::GuiElement::Compass;
+ else if (element == "copyright")
+ m_currentElement = GuiSkin::GuiElement::Copyright;
+ else if (element == "country_status")
+ m_currentElement = GuiSkin::GuiElement::CountryStatus;
+ else
+ ASSERT(false, ());
+ }
+ else if (m_inConfiguration == false)
+ {
+ if (element == "portrait" || element == "landscape")
+ m_inConfiguration = true;
+ else
+ ASSERT(false, ());
+ }
+ else
+ {
+ if (element == "anchor")
+ m_parser.SetElement(ResolverParser::Element::Anchor);
+ else if (element == "relative")
+ m_parser.SetElement(ResolverParser::Element::Relative);
+ else if (element == "offset")
+ m_parser.SetElement(ResolverParser::Element::Offset);
+ else
+ ASSERT(false, ());
+ }
+
+ return true;
+ }
+
+ void Pop(string const & element)
+ {
+ if (element == "anchor" || element == "relative" || element == "offset")
+ m_parser.SetElement(ResolverParser::Element::Empty);
+ else if (element == "portrait")
+ {
+ m_skin[m_currentElement].first = m_parser.GetResolver();
+ m_parser.Reset();
+ m_inConfiguration = false;
+ }
+ else if (element == "landscape")
+ {
+ m_skin[m_currentElement].second = m_parser.GetResolver();
+ m_parser.Reset();
+ m_inConfiguration = false;
+ }
+ else if (element == "ruler" || element == "compass" ||
+ element == "copyright" || element == "country_status")
+ {
+ m_inElement = false;
+ }
+ }
+
+ void AddAttr(string const & attribute, string const & value)
+ {
+ m_parser.Parse(attribute, value);
+ }
+
+ void CharData(string const &) {}
+
+private:
+ bool m_inConfiguration = false;
+ bool m_inElement = false;
+
+ GuiSkin::GuiElement m_currentElement = GuiSkin::GuiElement::Ruler;
+ ResolverParser m_parser;
+
+ map > & m_skin;
+};
+
+}
+
+GuiPosition PositionResolver::Resolve(int w, int h) const
+{
+ float resultX = w / 2.0f;
+ float resultY = h / 2.0f;
+ m2::PointF offset = m_offset * df::VisualParams::Instance().GetVisualScale();
+
+ if (m_resolveAnchor & dp::Left)
+ resultX = offset.x;
+ else if (m_resolveAnchor & dp::Right)
+ resultX = w - offset.x;
+ else
+ resultX += offset.x;
+
+ if (m_resolveAnchor & dp::Top)
+ resultY = offset.y;
+ else if (m_resolveAnchor & dp::Bottom)
+ resultY = h - offset.y;
+ else
+ resultY += offset.y;
+
+ return GuiPosition(m2::PointF(resultX, resultY), m_elementAnchor);
+}
+
+void PositionResolver::SetAnchorVertical(dp::Anchor anchor)
+{
+ m_elementAnchor = MergeAnchors(m_elementAnchor, anchor);
+}
+
+void PositionResolver::SetAnchorHorizontal(dp::Anchor anchor)
+{
+ m_elementAnchor = MergeAnchors(m_elementAnchor, anchor);
+}
+
+void PositionResolver::SetRelativeVertical(dp::Anchor anchor)
+{
+ m_resolveAnchor = MergeAnchors(m_resolveAnchor, anchor);
+}
+
+void PositionResolver::SetRelativeHorizontal(dp::Anchor anchor)
+{
+ m_resolveAnchor = MergeAnchors(m_resolveAnchor, anchor);
+}
+
+void PositionResolver::SetOffsetX(float x)
+{
+ m_offset.x = x;
+}
+
+void PositionResolver::SetOffsetY(float y)
+{
+ m_offset.y = y;
+}
+
+GuiSkin::GuiSkin(ReaderPtr const & reader)
+{
+ SkinLoader loader(m_resolvers);
+ ReaderSource > source(reader);
+ if (!ParseXML(source, loader))
+ LOG(LERROR, ("Error parsing gui skin"));
+}
+
+GuiPosition GuiSkin::ResolvePosition(GuiElement name)
+{
+ TResolversPair const & resolvers = m_resolvers[name];
+ PositionResolver const & resolver = (m_displayWidth < m_displayHeight) ? resolvers.first : resolvers.second;
+ return resolver.Resolve(m_displayWidth, m_displayHeight);
+}
+
+void GuiSkin::Resize(int w, int h)
+{
+ m_displayWidth = w;
+ m_displayHeight = h;
+}
+
+ReaderPtr ResolveGuiSkinFile(string const & deviceType)
+{
+ Platform & pl = GetPlatform();
+ ReaderPtr reader;
+ try
+ {
+ reader = pl.GetReader("resource-default/" + deviceType + ".ui");
+ }
+ catch(FileAbsentException & e)
+ {
+ LOG(LINFO, ("Gui skin for : ", deviceType ,"not found"));
+ }
+
+ if (reader.GetPtr() == 0)
+ {
+ try
+ {
+ reader = pl.GetReader("resource-default/default.ui");
+ }
+ catch(FileAbsentException & e)
+ {
+ LOG(LINFO, ("Default gui skin not found"));
+ throw e;
+ }
+ }
+
+ return reader;
+}
+
+}
diff --git a/drape_frontend/gui_skin.hpp b/drape_frontend/gui_skin.hpp
new file mode 100644
index 0000000000..fe8d32b827
--- /dev/null
+++ b/drape_frontend/gui_skin.hpp
@@ -0,0 +1,70 @@
+#pragma once
+
+#include "../coding/reader.hpp"
+
+#include "../drape/drape_global.hpp"
+#include "../geometry/point2d.hpp"
+
+#include "../std/map.hpp"
+
+namespace df
+{
+
+struct GuiPosition
+{
+ GuiPosition(m2::PointF const & pt, dp::Anchor anchor)
+ : m_pixelPivot(pt)
+ , m_anchor(anchor) {}
+
+ m2::PointF const m_pixelPivot;
+ dp::Anchor const m_anchor;
+};
+
+class PositionResolver
+{
+public:
+ PositionResolver() = default;
+
+ GuiPosition Resolve(int w, int h) const;
+ void SetAnchorVertical(dp::Anchor anchor);
+ void SetAnchorHorizontal(dp::Anchor anchor);
+ void SetRelativeVertical(dp::Anchor anchor);
+ void SetRelativeHorizontal(dp::Anchor anchor);
+ void SetOffsetX(float x);
+ void SetOffsetY(float y);
+
+private:
+ dp::Anchor m_elementAnchor = dp::Center;
+ dp::Anchor m_resolveAnchor = dp::Center;
+ m2::PointF m_offset = m2::PointF::Zero();
+};
+
+class GuiSkin
+{
+public:
+ enum class GuiElement
+ {
+ CountryStatus,
+ Ruler,
+ Compass,
+ Copyright
+ };
+
+ GuiSkin(ReaderPtr const & reader);
+
+ GuiPosition ResolvePosition(GuiElement name);
+ void Resize(int w, int h);
+
+private:
+ /// TResolversPair.first - Portrait (when weight < height)
+ /// TResolversPair.second - Landscape (when weight >= height)
+ typedef pair TResolversPair;
+ map m_resolvers;
+
+ int m_displayWidth;
+ int m_displayHeight;
+};
+
+ReaderPtr ResolveGuiSkinFile(string const & deviceType);
+
+}