diff --git a/modules/juce_gui_basics/drawables/juce_SVGParser.cpp b/modules/juce_gui_basics/drawables/juce_SVGParser.cpp index 2a3e5e51e8..5fec572261 100644 --- a/modules/juce_gui_basics/drawables/juce_SVGParser.cpp +++ b/modules/juce_gui_basics/drawables/juce_SVGParser.cpp @@ -245,7 +245,7 @@ public: case 'H': case 'h': - if (parseCoord (d, p1.x, false, true)) + if (parseCoord (d, p1.x, false, Axis::x)) { if (isRelative) p1.x += last.x; @@ -263,7 +263,7 @@ public: case 'V': case 'v': - if (parseCoord (d, p1.y, false, false)) + if (parseCoord (d, p1.y, false, Axis::y)) { if (isRelative) p1.y += last.y; @@ -491,7 +491,7 @@ private: if (tag == "g") return parseGroupElement (xml, true); if (tag == "svg") return parseSVGElement (xml); - if (tag == "text") return parseText (xml, true); + if (tag == "text") return parseText (xml, true, nullptr); if (tag == "image") return parseImage (xml, true); if (tag == "switch") return parseSwitch (xml); if (tag == "a") return parseLinkElement (xml); @@ -666,7 +666,7 @@ private: Drawable* parseUseOther (const XmlPath& xml) const { - if (auto* drawableText = parseText (xml, false)) return drawableText; + if (auto* drawableText = parseText (xml, false, nullptr)) return drawableText; if (auto* drawableImage = parseImage (xml, false)) return drawableImage; return nullptr; @@ -750,7 +750,7 @@ private: for (auto t = dashList.getCharPointer();;) { float value; - if (! parseCoord (t, value, true, true)) + if (! parseCoord (t, value, true, Axis::x)) break; dashLengths.add (value); @@ -1050,8 +1050,83 @@ private: return op.target; } + /* Handling the stateful consumption of x and y coordinates added to and elements. + + elements must have their own x and y attributes, or be positioned at (0, 0) since groups + enclosing elements can't have x and y attributes. + + elements can be embedded inside elements, and elements. elements + can't be embedded inside or elements. + + A element can have its own x, y attributes, which it will consume at the same time as + it consumes its parent's attributes. Its own elements will take precedence, but parent elements + will be consumed regardless. + */ + class StringLayoutState + { + public: + StringLayoutState (StringLayoutState* parentIn, Array xIn, Array yIn) + : parent (parentIn), + xCoords (std::move (xIn)), + yCoords (std::move (yIn)) + { + } + + Point getNextStartingPos() const + { + if (parent != nullptr) + return parent->getNextStartingPos(); + + return nextStartingPos; + } + + void setNextStartingPos (Point newPos) + { + nextStartingPos = newPos; + + if (parent != nullptr) + parent->setNextStartingPos (newPos); + } + + std::pair, std::optional> popCoords() + { + auto x = xCoords.isEmpty() ? std::optional{} : std::make_optional (xCoords.removeAndReturn (0)); + auto y = yCoords.isEmpty() ? std::optional{} : std::make_optional (yCoords.removeAndReturn (0)); + + if (parent != nullptr) + { + auto [parentX, parentY] = parent->popCoords(); + + if (! x.has_value()) + x = parentX; + + if (! y.has_value()) + y = parentY; + } + + return { x, y }; + } + + bool hasMoreCoords() const + { + if (! xCoords.isEmpty() || ! yCoords.isEmpty()) + return true; + + if (parent != nullptr) + return parent->hasMoreCoords(); + + return false; + } + + private: + StringLayoutState* parent = nullptr; + Point nextStartingPos; + Array xCoords, yCoords; + }; + Drawable* parseText (const XmlPath& xml, bool shouldParseTransform, - AffineTransform* additonalTransform = nullptr) const + AffineTransform* additonalTransform, + StringLayoutState* parentLayoutState = nullptr) const { if (shouldParseTransform && xml->hasAttribute ("transform")) { @@ -1067,10 +1142,11 @@ private: if (! xml->hasTagName ("text") && ! xml->hasTagNameIgnoringNamespace ("tspan")) return nullptr; - Array xCoords, yCoords; - - getCoordList (xCoords, getInheritedAttribute (xml, "x"), true, true); - getCoordList (yCoords, getInheritedAttribute (xml, "y"), true, false); + // If a element has no x, or y attributes of its own, it can still use the + // parent's yet unconsumed such attributes. + StringLayoutState layoutState { parentLayoutState, + getCoordList (*xml, Axis::x), + getCoordList (*xml, Axis::y) }; auto font = getFont (xml); auto anchorStr = getStyleAttribute (xml, "text-anchor"); @@ -1086,28 +1162,20 @@ private: const auto subtextElements = [&] { - std::vector> result; - - if (xCoords.size() == 1) - { - result.emplace_back (fullText, xCoords[0], yCoords[0]); - return result; - } + std::vector, std::optional>> result; - if (xCoords.size() != yCoords.size() || fullText.length() != yCoords.size()) + for (auto it = fullText.begin(), end = fullText.end(); it != end;) { - jassertfalse; - result.emplace_back (fullText, xCoords[0], yCoords[0]); - return result; + const auto pos = layoutState.popCoords(); + const auto next = layoutState.hasMoreCoords() ? it + 1 : end; + result.emplace_back (String (it, next), pos.first, pos.second); + it = next; } - for (int i = 0; i < xCoords.size(); ++i) - result.emplace_back (fullText.substring (i, i + 1), xCoords[i], yCoords[i]); - return result; }(); - for (const auto& [text, x, y] : subtextElements) + for (const auto& [text, optX, optY] : subtextElements) { auto dt = new DrawableText(); dc->addAndMakeVisible (dt); @@ -1123,6 +1191,9 @@ private: dt->setColour (parseColour (xml, "fill", Colours::black) .withMultipliedAlpha (parseSafeFloat (getStyleAttribute (xml, "fill-opacity", "1")))); + const auto x = optX.value_or (layoutState.getNextStartingPos().getX()); + const auto y = optY.value_or (layoutState.getNextStartingPos().getY()); + Rectangle bounds (x, y - font.getAscent(), font.getStringWidthFloat (text), font.getHeight()); @@ -1130,11 +1201,13 @@ private: else if (anchorStr == "end") bounds.setX (bounds.getX() - bounds.getWidth()); dt->setBoundingBox (bounds); + + layoutState.setNextStartingPos ({ bounds.getRight(), y }); } } else if (e->hasTagNameIgnoringNamespace ("tspan")) { - dc->addAndMakeVisible (parseText (xml.getChild (e), true)); + dc->addAndMakeVisible (parseText (xml.getChild (e), true, nullptr, &layoutState)); } } @@ -1263,7 +1336,9 @@ private: } //============================================================================== - bool parseCoord (String::CharPointerType& s, float& value, bool allowUnits, bool isX) const + enum class Axis { x, y }; + + bool parseCoord (String::CharPointerType& s, float& value, bool allowUnits, Axis axis) const { String number; @@ -1273,14 +1348,14 @@ private: return false; } - value = getCoordLength (number, isX ? viewBoxW : viewBoxH); + value = getCoordLength (number, axis == Axis::x ? viewBoxW : viewBoxH); return true; } bool parseCoords (String::CharPointerType& s, Point& p, bool allowUnits) const { - return parseCoord (s, p.x, allowUnits, true) - && parseCoord (s, p.y, allowUnits, false); + return parseCoord (s, p.x, allowUnits, Axis::x) + && parseCoord (s, p.y, allowUnits, Axis::y); } bool parseCoordsOrSkip (String::CharPointerType& s, Point& p, bool allowUnits) const @@ -1319,13 +1394,26 @@ private: return getCoordLength (xml->getStringAttribute (attName), sizeForProportions); } - void getCoordList (Array& coords, const String& list, bool allowUnits, bool isX) const + Array getCoordList (const XmlElement& xml, Axis axis) const + { + const String attributeName { axis == Axis::x ? "x" : "y" }; + + if (! xml.hasAttribute (attributeName)) + return {}; + + return getCoordList (xml.getStringAttribute (attributeName), true, axis); + } + + Array getCoordList (const String& list, bool allowUnits, Axis axis) const { auto text = list.getCharPointer(); float value; + Array coords; - while (parseCoord (text, value, allowUnits, isX)) + while (parseCoord (text, value, allowUnits, axis)) coords.add (value); + + return coords; } static float parseSafeFloat (const String& s)