Rectangles and lineThickness

I’m a bit lost about rectangle and line thickness…

As a quick test, I just change LookAndFeel_V3::drawButtonBackground to simply draw a rectangle :

void LookAndFeel_V3::drawButtonBackground (Graphics& g, Button& button, const Colour& /*backgroundColour*/,
                                           bool /*isMouseOverButton*/, bool /*isButtonDown*/)
{
    g.setColour (Colours::black);
    const Rectangle<float> area (button.getLocalBounds().toFloat());
    g.drawRect (area, 1.f);
}

Here is the result :

Now if I just change it to a rounded rectangle with a corner of 0, I got a thinner rectangle!

void LookAndFeel_V3::drawButtonBackground (Graphics& g, Button& button, const Colour& /*backgroundColour*/,
                                           bool /*isMouseOverButton*/, bool /*isButtonDown*/)
{
    g.setColour (Colours::black);
    const Rectangle<float> area (button.getLocalBounds().toFloat());
    g.drawRoundedRectangle (area, 0.f, 1.f);
}

Shouldn’t they lead to the same result?

Also, I can’t get such a thin rectangle If I just do the same in a paint method in my parent component (here I’m just testing that in the MenusDemo) :

void paint (Graphics& g) override
{
    g.fillAll (Colour (0xffeeeeee));
    g.setColour (Colours::black);

    const juce::Rectangle<float> area (5.f, 80.f, 90.0f, 20.f);
    g.drawRoundedRectangle (area, 0.f, 1.f);
    g.drawRect (area.translated (100.f, 0.f), 1.f);

    // test the same thing with a 0.5 offset
    const juce::Rectangle<float> area2 (5.5f, 110.5f, 90.0f, 20.f);
    g.drawRoundedRectangle (area2, 0.f, 1.f);
    g.drawRect (area2.translated (100.f, 0.f), 1.f);
}

Why can’t I get the same result, what am I missing here?

1 Like

Hi lalala,

Let me summarise your findings in a screenshot (including retina and non-retina displays):

Let me first try to explain the difference between roundedRect and drawRect. roundedRect uses a path internally to draw the rounded rect. You can get the same result with the following code:

Path p;
p.addRectangle (area.getX(), area.getY(), area.getWidth(), area.getHeight());
g.strokePath (p, PathStrokeType(1.f));

If you think about it, the expected result of drawing a path with line thickness 1 centred at whole-numbered pixel position (labeled “No offset” in the image above) should result in a fuzzy rectangle, as the path itself is centred on a pixel boundary, and therefore, 50% of the stroke will be in one pixel and the remaining in the adjacent pixel. This is also what vector graphics programs will do.

Now, if JUCE would implement the drawRect function with paths then this would confuse the majority of users. As drawing a simple integral rectangle with drawRect would always result in “fuzzy” rectangles. Therefore the drawRect method will in fact inset the rectangle by 0.5 pixels. Therefore the fuzziness of path vs drawRect switches around if you draw both with an offset of 0.5 pixels. Does this make sense?

Now on to why the LookAndFeel method gives you a thin rectangle. This is because the drawing is clipped to the bounds of the ToggleButton. In fact, without the clipping, 50% of the stroke would be outside the ToggleButton bounds and you would get the same result as your paint method. To achieve the same thin rectangle as the LookAndFeel case you could do the following:

void paint (Graphics& g)
{
    g.fillAll (Colour (0xffeeeeee));
    g.setColour (Colours::black);

    const juce::Rectangle<float> area (5.f, 80.f, 90.0f, 20.f);
    
    g.saveState();
    g.reduceClipRegion (area.toNearestInt());
        
    Path p;
    p.addRectangle (area.getX(), area.getY(), area.getWidth(), area.getHeight());
    g.drawRoundedRectangle (area, 0.f, 1.f);
    g.strokePath (p, PathStrokeType(1.f));
        
    g.restoreState();
}
6 Likes

Thanks a lot for writing such a complete and clear explanation Fabian!