As the title does not say, there is a bug in Path addBubble when the bubble’s tip is not on the bottom, because the code create rectangle with negative width, and Rectangle::contains complains with them.
The following code fixes it:
void Path::addBubble (const Rectangle<float>& bodyArea,
const Rectangle<float>& maximumArea,
const Point<float>& arrowTip,
const float cornerSize,
const float arrowBaseWidth)
{
const float cornerSize2 = 2.0f * cornerSize;
startNewSubPath (bodyArea.getX() + cornerSize, bodyArea.getY());
float targetLimitX = bodyArea.getX() + cornerSize + arrowBaseWidth;
float targetLimitW = bodyArea.getWidth() - cornerSize2 - arrowBaseWidth * 2.0f;
+ if(targetLimitW < 0)
+ {
+ targetLimitX = targetLimitX + targetLimitW;
+ targetLimitW = -targetLimitW;
+ }
float targetLimitY = bodyArea.getY() + cornerSize + arrowBaseWidth;
float targetLimitH = bodyArea.getHeight() - cornerSize2 - arrowBaseWidth * 2.0f;
+ if(targetLimitH < 0)
+ {
+ targetLimitY = targetLimitY + targetLimitH;
+ targetLimitH = -targetLimitH;
+ }
if (Rectangle<float> (targetLimitX, maximumArea.getY(),
targetLimitW, bodyArea.getY() - maximumArea.getY()).contains (arrowTip))
{
lineTo (arrowTip.x - arrowBaseWidth, bodyArea.getY());
lineTo (arrowTip.x, arrowTip.y);
lineTo (arrowTip.x + arrowBaseWidth, bodyArea.getY());
}
lineTo (bodyArea.getRight() - cornerSize, bodyArea.getY());
addArc (bodyArea.getRight() - cornerSize2, bodyArea.getY(), cornerSize2, cornerSize2, 0, float_Pi * 0.5f);
if (Rectangle<float> (bodyArea.getRight(), targetLimitY,
maximumArea.getRight() - bodyArea.getRight(), targetLimitH).contains (arrowTip))
{
lineTo (bodyArea.getRight(), arrowTip.y - arrowBaseWidth);
lineTo (arrowTip.x, arrowTip.y);
lineTo (bodyArea.getRight(), arrowTip.y + arrowBaseWidth);
}
lineTo (bodyArea.getRight(), bodyArea.getBottom() - cornerSize);
addArc (bodyArea.getRight() - cornerSize2, bodyArea.getBottom() - cornerSize2, cornerSize2, cornerSize2, float_Pi * 0.5f, float_Pi);
if (Rectangle<float> (targetLimitX, bodyArea.getBottom(),
targetLimitW, maximumArea.getBottom() - bodyArea.getBottom()).contains (arrowTip))
{
lineTo (arrowTip.x + arrowBaseWidth, bodyArea.getBottom());
lineTo (arrowTip.x, arrowTip.y);
lineTo (arrowTip.x - arrowBaseWidth, bodyArea.getBottom());
}
lineTo (bodyArea.getX() + cornerSize, bodyArea.getBottom());
addArc (bodyArea.getX(), bodyArea.getBottom() - cornerSize2, cornerSize2, cornerSize2, float_Pi, float_Pi * 1.5f);
if (Rectangle<float> (maximumArea.getX(), targetLimitY, bodyArea.getX() - maximumArea.getX(), targetLimitH).contains (arrowTip))
{
lineTo (bodyArea.getX(), arrowTip.y + arrowBaseWidth);
lineTo (arrowTip.x, arrowTip.y);
lineTo (bodyArea.getX(), arrowTip.y - arrowBaseWidth);
}
lineTo (bodyArea.getX(), bodyArea.getY() + cornerSize);
addArc (bodyArea.getX(), bodyArea.getY(), cornerSize2, cornerSize2, float_Pi * 1.5f, float_Pi * 2.0f - 0.05f);
closeSubPath();
}
// Example code that's failing without the change:
Path path;
juce::Rectangle<float> bodyAreaRect(0, 0, static_cast<float>(getWidth()), static_cast<float>(getHeight() / 2));
juce::Rectangle<float> maxAreaRect(0, 0, static_cast<float>(getWidth()), static_cast<float>(getHeight()));
juce::Point<float> tipPosPoint(static_cast<float>(getWidth() / 2) , static_cast<float>(getHeight() - 1));
float cornerSize = static_cast<float>(getWidth() / 3);
float arrowBaseWidth = static_cast<float>(getWidth() / 4);
path.addBubble(bodyAreaRect, maxAreaRect, tipPosPoint, cornerSize, arrowBaseWidth);
g.setColour(backgroundColor);
g.fillPath(path);
g.setColour(edgeColor);
PathStrokeType type(1);
g.strokePath(path, type);