Tricks to set components bounds with floats instead of ints

I think some people might be interested in this.

I’ve been fighting with the fact that Component::setBounds accepts only integer arguments for years, preventing me from getting pixel-perfect alignement of UI elements with rescaling enabled. Recently, I did code some solutions that allowed me to get the results I wanted as elegantly as possible without having to hack anything in the JUCE code base.

The main idea is the following: we want a function such as setFloatBounds(float x, float y, float width, float height) for our UI components, and a way to handle this information in the associated drawing functions. It is useful when UI design is either skeumorphic (with say a big background PNG where each widget needs to be in a specific place) or vectorial-based so the spacing between elements is always the same whatever the scale factor, specifically if you use a big SVG file and align everything around it.

The function setFloatBounds(float x, float y, float width, float height) stores the float coordinates somewhere, and set the regular existing integer coordinates as well, so the float bounds rectangle can be inserted inside the integer bounds rectangle, for example like this:

void CustomComponent::setFloatBounds (float newFloatX, float newFloatY, float newFloatWidth, float newFloatHeight)
{
    floatX = newFloatX;
    floatY = newFloatY;
    floatWidth = newFloatWidth;
    floatHeight = newFloatHeight;

    auto newIntX = juce::roundToInt (std::floor (floatX));
    auto newIntY = juce::roundToInt (std::floor (floatY));
    auto newIntRight = juce::roundToInt (std::ceil (floatX + floatWidth));
    auto newIntBottom = juce::roundToInt (std::ceil (floatY + floatHeight));

    setBounds (newIntX, newIntY, newIntRight - newIntX, newIntBottom - newIntY);
}

Then, the drawing function will have to use the float information, such as the offsets between the float rectangle and integer rectangle coordinates, and float drawing methods. For example, here is what I do for drawing an image and some text:

void CustomComponent::paint (juce::Graphics &g)
{
    auto rect = juce::Rectangle<float> (floatX - getX(), floatY - getY(), floatWidth, floatHeight);

    juce::AffineTransform t = juce::RectanglePlacement (juce::RectanglePlacement::stretchToFit)
        .getTransformToFit (image->getBounds().toFloat(), rect);

    g.drawImageTransformed (*image, t, false);

    // To Do : set the colours, fonts, grab the text to display etc.

    juce::GlyphArrangement arr;

    arr.addFittedText (fontText, strTextToDisplay,
        rect.getX(), rect.getY(),
        rect.getWidth(), rect.getHeight(),
        juce::Justification::centred,
        4,
        0.f);

    arr.draw (g);
}

In the case of text drawing, there is no Graphics function which allowed what I needed, but I found out that the code inside Graphics::drawFittedText can be copied and pasted in my drawing function, and customized to do what I need.

Now, where should you put that code to? I did it in two ways for now, maybe you’ll find better solutions.

  1. I created some custom components in my code, so it’s easy to add those functionalities, since I don’t use LookAndFeel methods there anyway.

  2. For widgets like Sliders, I could do the same, by copying and pasting the Slider class code in my project files, and adding the functionalities. Instead, I created new LookAndFeel classes, where I can store the float bounds. That means I need one LF object for each component, which is not so bad since they can be of the same custom LF class. So that’s that LF class which gets the new setFloatBounds and painting functions. What I did also in that case is creating new functions directly in the PluginEditor class such as this one:

void ProcessorEditor::setFloatBoundsFor (Slider *slider, CustomSliderLookAndFeel *lf, float floatX, float floatY, float floatWidth, float floatHeight)
{
    auto newIntX = juce::roundToInt (std::floor (floatX));
    auto newIntY = juce::roundToInt (std::floor (floatY));
    auto newIntRight = juce::roundToInt (std::ceil (floatX + floatWidth));
    auto newIntBottom = juce::roundToInt (std::ceil (floatY + floatHeight));

    slider->setBounds (newIntX, newIntY, newIntRight - newIntX, newIntBottom - newIntY);
    lf->setFloatBounds (true, floatX, floatY, floatWidth, floatHeight);
}

So, in the resize function, I can do something like this:

void ProcessorEditor::resized()
{
    auto widthImage = imageBackground.getWidth();
    auto heightImage = imageBackground.getHeight();

    auto scaleFactorX = getWidth() / (float) widthImage;
    auto scaleFactorY = getHeight() / (float) heightImage;

    setFloatBoundsFor (sliderVolume.get(), theVolumeKnobLF.get(),
        JUCE_LIVE_CONSTANT (0.7285f) * scaleFactorX * widthImage,
        JUCE_LIVE_CONSTANT (0.1045f) * scaleFactorY * heightImage,
        theVolumeKnobLF->getStripWidth() * scaleFactorX,
        theVolumeKnobLF->getStripHeight() * scaleFactorY);
}

As you can see, I use the macro JUCE_LIVE_CONSTANT to set the XY position of the element (could be automated with image processing techniques or whatever), and I have my own LF class to load PNG strips. But more importantly, I don’t use the same scale factor here for the X and Y coordinates. That’s because the whole PluginEditor size needs to be integer, so the X/Y ratio might be off depending on the scale factor. This way, I use the current X/Y ratio instead of the ideal one to set the position of my components.

I hope that will help you people! Tell me if you are using my tricks, or if you have ideas to improve the whole scheme.

9 Likes

nice! Can you share some before/after screenshots so we can see the difference this provides?

What benefits does this have over placing all the elements in a container component at their respective integer design pixel positions and using setTransform() on the container component to do the scaling?

2 Likes

All other opinions aside, I think you can spare having a LF instance for every Component, if its sole purpose is to store those float coordinates.
You can store those float coordinates directly in the Component properties (Component::getProperties()) and have only a single LF instance per widget type, that reads those coordinates from each component when it’s drawing it

1 Like

Hello! Well, it’s not that useful to show a screenshot for this, unless I can capture in one of the projects I’m working on a problem, and how doing this solves it. However, if you do some testing with this kind of techniques, you’ll see that you you won’t notice anymore sudden steps while increasing the float sizes.

Oh yes you’re right, it would be better this way, thanks for the suggestion!

Originally, I did all of this because I remembered I had a talk with @jules a long time ago about the possibility of making the regular Component::setBounds takes float arguments instead of integer arguments only, so I developed my idea this way. But honestly you’re right, it is way more simple to just use Component::setTransform to get the same result. I discussed this with @eyalamir and he suggested I could do something like this:

AffineTransform getRectTransform (const juce::Rectangle<float>& source, const juce::Rectangle<float>& target)
{
    float newX = target.getX();
    float newY = target.getY();

    float scaleX = target.getWidth() / source.getWidth();
    float scaleY = target.getHeight() / source.getHeight();

    return juce::AffineTransform::translation (-source.getX(), -source.getY())
        .scaled (scaleX, scaleY)
        .translated (newX, newY);
}

void setTransformedBounds (juce::Component *comp, const juce::Rectangle<float>& bounds)
{
    auto compBounds = bounds.getSmallestIntegerContainer();
    auto transform = getRectTransform (compBounds.toFloat(), bounds);

    comp->setTransform (juce::AffineTransform());
    comp->setBounds (compBounds);
    comp->setTransform (transform);
}

Then your drawing functions can just draw everything using integer boundaries, the float part is handled by the transformation automatically.

Thanks everybody for all the suggestions!

2 Likes

By the way, I’ve just come across a case where using Component::setTransform() and the setTransformedBounds doesn’t do the trick alone.

Let’s say you have a component which does only one thing in Component::paint, drawing a rectangle using Graphics::fillRect. If you try to set “float bounds”, it’s not going to work at all, you’ll see very easily that the apparent size changes when you increase the float bounds are stepped instead of continuous.

However, if you use in the component Graphics::fillRoundedRectangle, which takes a Rectangle<float> instead of integer coordinates, the setTransformedBounds based on Component::setTransform trick works.

So I wonder if using blindly setTransform to solve the problem described above is a good thing, and more specifically what are the cases where it doesn’t work…

1 Like

Apparently, one of the cases where setTransformedBounds doesn’t do the trick is components using text functions, such as Label or anything drawing text. I had to create this function and use it as much as possible to get the transformations right:

static const void drawFittedTextFloat(juce::Graphics &g, const juce::String& text, juce::Rectangle<float> area,
    juce::Justification justification,
    const int maximumNumberOfLines,
    const float minimumHorizontalScale = 0.0f)
{
    if (text.isNotEmpty() && (!area.isEmpty()) && g.getInternalContext().clipRegionIntersects (area.toNearestInt()))
    {
        juce::GlyphArrangement arr;
        arr.addFittedText (g.getInternalContext().getFont(), text,
            area.getX(), area.getY(),
            area.getWidth(), area.getHeight(),
            justification,
            maximumNumberOfLines,
            minimumHorizontalScale);

        arr.draw (g);
    }
}
1 Like

Are you on Windows or Mac?

Windows

As far as I recall the software renderer (which is used on Windows) doesn’t do AA for int rects and cliprects. I think this is actually the better default but ideally AA should be somehow user configurable. This is why: Unwanted borders when scaling UI with opaque components - #21 by PixelPacker - so as things currently are, using setTransform() and fillRect() or fillAll() does what you want on Mac, but doesn’t on windows. And I want it to not do what you want either way :wink: (unless you use fillRect(Rectangle) of course).

I don’t know the current state of affairs of this, but you could try to enable JUCE_DIRECT2D and see if this changes anything regarding AA. Or avoid fills with int rects and fill float rects or paths instead (as you’ve already discovered).

1 Like

Thanks to @IvanC - only just found this, incredibly useful!

Pete

1 Like

Question for the JUCE devs (@reuk?) - are there plans with JUCE 7 to extend setBounds to work with floating point, so we can avoid having to use this sort of hack? :slight_smile:

Best wishes, Pete

3 Likes