Expand the painting/child area of a component

This is a constant pain, I would like to be able to draw borders, text, icons, marks, effects as shadows, or any graphics that are total o partial outside the region of the component, but it seems to be impossible.

The logical solution is to enlarge the dimensions of the component, but then all the drawings and positions that depend on those dimensions will be out of control. Also these dimensions would overlap with other components which makes alignment and maintenance difficult, in the end it is more pain.

Is there any reasonable solution?, I need something like:

setPaintBounds(getBounds.expanded(20,20));

I have seen that setPaintingIsUnclipped(true) allows there to be no drawing limits, if there is no other solution this can help, but maybe it would be better if the clipping limits could be set

Components are hierarchical – you can keep the component that you already have, but create a new, larger component that will contain it (as a child component) at any location within that new parent component that makes sense for you. Set the location of the component you wish were larger but not really in the parent component’s resized() method. If you need to have that enclosing component paint on top of its child, override its paintOverChildren() method.

I have tried setting setPaintingIsUnclipped(true) on the parent container and this allows child components to be displayed without the need to make the parent component larger.

It doesn’t seem the most optimal solution to completely disable clipping, but I don’t seem to have any other alternative, since I don’t want to arbitrarily modify the size of all the components in the hierarchy as this would make me lose control of everything.

Anyway I still think that Juce components internally should have independent limits for the component and for the drawing. Since many drawings can exceed the component limit without necessarily belonging to the component limits. The best example of this is effects like shadows, glows, or flares.

Setting setPaintingIsUnclipped (true) is not going to do the right thing in the long run: as the doc says, you can do that only if you are absolutely sure that your component will not paint outside of its bounds. It’s for having better drawing performances, and if you break the contract and paint outside of those bounds, you will almost certainly have painting artifacts sooner or later, e.g. the parts being drawn outside of the component becoming out of sync with what’s painted inside, or being wiped out when the parent component gets painted again, that kind of stuff.

The solution provided by @bgporter sounds sensible, that’s how I would do it myself: put your “main” component inside a larger component that paints the “decorations” for your main component

1 Like

This solution involves the pain I was talking about in the main message.

I am now trying another way that might be better, which is to use a container class that keeps the boundaries. So far I’ve created a new setBounds function that receives the component’s alignment bounds and the clipping bounds, and then sets the juce::Component bound to the position of the alignment bound. It seems to work fine.

class Container : public juce::Component
{
    juce::Rectangle<int> bounds;

public:
    void setBounds(juce::Rectangle<int> newBounds, juce::Rectangle<int> clippingBounds)
    {
        juce::Component::setBounds(clippingBounds);
        juce::Component::setTopLeftPosition(clippingBounds.getPosition() + (newBounds.getPosition() - clippingBounds.getPosition()) );
        bounds = newBounds;
    }

    juce::Rectangle<int> getBounds()
    {
        return bounds;
    }
};

now the components will no longer paint beyond the allowed border of the parent component, however I keep setPaintingIsUnclipped(true) for the basic components. I can’t make a class for them since many functions of the Component class related to positions and sizes are involved.

That’s exactly how my PluginGuiMagic works btw.

Painting outside the component bounds does not only break the contract, it won’t happen, because the component bounds are used to determine when to update/paint at all. The area outside will not be considered, which usually means you get frozen leftovers from accidental paintings. E.g. once the user drags a window across, you will only see lines… looks funny, but certainly not what you want.

My class would look like that:

class Decorator : public juce::Component
{
  public:
    Decorator (std::unique_ptr<juce::Component> comp)
      : item (comp)
    {
        addAndMakeVisible (item.get());
    }
    void resized() override
    {
        item->setBounds (getLocalBounds().reduced (margin));
    }
    void paint (juce::Graphics& g) override
    {
        // draw your decorations, e.g.
        g.fillAll (juce::Colours::black);
        g.setColour (juce::Colours::red);
        g.drawRoundedRectangle (getLocalBounds().reduced (margin / 2), 8.0f, 2.0f);
    }
    juce::Component* get()
    {
        return item.get();
    }

  private:
    std::unique_ptr<juce::Component> item;
    int margin = 5;
};
1 Like

I am trying a new approach, and it seems to be working well. It involves adding a frame class to components that require it. The component drawing will be expanded outside its boundaries with setPaintingIsUnclipped, but it will be confined to its frame.

class Frame
{
    juce::Component frame;
    juce::Component* component;

public:

  Frame(juce::Component* c) : component(c)
  {
  }

  juce::Component& addAndMakeVisibleFrame()
  {
      frame.addAndMakeVisible(component);
      component->setPaintingIsUnclipped(true);
      return frame;
  }

  void setFrameBounds(juce::Rectangle<int> compBounds, int marginLeft, int marginRight, int marginTop, int marginBottom)
  {
      component->setBounds(marginLeft, marginTop, compBounds.getWidth(), compBounds.getHeight());

      compBounds.setX(compBounds.getX() - marginLeft);
      compBounds.setWidth(compBounds.getWidth() + marginLeft + marginRight);
      compBounds.setY(compBounds.getY() - marginTop);
      compBounds.setHeight(compBounds.getHeight() + marginTop + marginBottom);
      frame.setBounds(compBounds);
    }
};

the only update needed is the way in which the component is added (since it is now the frame that depends on the parent component), and the setting of its bounds, which requires some calculations to establish the extension of the frame with respect to the component.

    addAndMakeVisible(myComponent->addAndMakeVisibleFrame());

    myComponent->setFrameBounds(bounds, 0, 0, 0, 10);

is not only decorations or effects, having an additional border avoids any type of defects in drawings that for whatever reason go out of the limits of the component. For example when drawing with thicknesses.

clipped

extending the border 3 pixels by adding to the component the frame class

unclipped

My solution to this is to create a PaddedComponent that I use instead of the vanilla juce Component.

I specify component bounds with myComponent.setBoundsReducedByPadding (getLocalBounds(), 50, 50) (which “eats” some of the component bounds for padding)
and myComponent.setBoundsAndPadding (area, 0, 20, 0, 0) (which expands the component bounds to add padding).

Whenever I would use getLocalBounds, I now use getContentBounds. For example, instead of g.fillAll (which would fill the padding) I use g.fillRect (getContentBounds()).

A byproduct of building a whole app this way is that you often end up with overlapping components. I worried about this at first and overrode hitTest to make sure padding was never clickable, but then I realized it’s actually best UX practice to have larger click areas than the visible icon/button where possible. So it’s worked out well.

Note: I’m only storing the padding values in component properties (vs class members) in order to display the padding in my component inspector.

IMO the framework should provide infrastructure for common UI layouting like this, as implementing any “modern” design currently requires working against the framework instead of with it. With luck the move to HTML in will render this moot since web UI layouting is much more flexible and handles these things behind the scenes.

3 Likes

I think it is something similar to what I have implemented. The problem with this is that the parent components must also take padding into account. maybe all the padding in the hierarchy should increase dynamically with the size of the padding of the child components

Yeah, my solution increases the size of each component with padding (vs. adding a frame/wrapper component).

Not 100% sure how this is a problem… if a parent component needs to be larger because of a child it contains, I can add padding…(the total size a component includes the padding)

due to the issues related to boundaries or positions I have abandoned any kind of modifications or convoluted additions to my classes, and finally I have opted for the hardest, but the cleanest and most effective solution.

I added a paintDecorations function in juce::Slider, which can be overridden in any component. This function iis called within the loop of child components of Component::paintComponentAndChildren just before clipping, so it uses the boundaries of the parent component to paint.

I think the developers should implement something similar that allows drawing at least up to the boundaries of the parent component, currently it is not possible to do something like this in the normal paint function.

void paintDecorations(juce::Graphics& g) override
{ 
    g.drawText("GAIN", 0, -16, 40, 20, juce::Justification::centred);
    g.drawRoundedRectangle(-10,-20, getWidth()+20, getHeight()+40, 3.0f, 2.0f);
}

You could easily do that in the LookAndFeel.

Simply override the drawRotarySlider. You can use the slider.getProperties() to set the text and the specs like radius for the rounded rectangle.

the idea is to set the boundaries just for the control, in this example case a vertical slider, and freely draw associated elements such as frames, marks, text, or anything that goes outside the slider boundaries. As far as I know lookAndFeel does not allow drawing outside the slider boundaries.

No, but you can make the decoration a part of the Slider

Extending the limits of control to arbitrary sizes? I have never liked this. this in my opinion is ugly and impractical. and imposes limitations and constant issues. What if the title is too long, 2 or 3 times the width of the slider, I have to recompose everything creating unnecessarily wide sliders. I prefer to paint freely in calls to paintDecoration from the parent component using its limits.

Seems I need to show code to explain. Just copy the getSliderLayout() and reduce the returned bounds when the variable localBounds is calculated (and some fixes when setting the textBox, because it assumes x and y to be 0)

In the drawRotarySlider you can simply draw your decorations around.

// == constructor ==

MainComponent::MainComponent()
{
    setLookAndFeel(&lookAndFeel);
    
    addAndMakeVisible(gain);
    addAndMakeVisible(pan);
    
    gain.getProperties().set("title", "Gain");
    pan.getProperties().set("title", "Pan");

    setSize (600, 400);
}

// == LookAndFeel ==

#pragma once

#include <juce_gui_basics/juce_gui_basics.h>

class DecoLookAndFeel : public juce::LookAndFeel_V2
{
public:
    static constexpr auto kDecoSpace = 20;
    
    DecoLookAndFeel() = default;
    
    void drawRotarySlider (juce::Graphics& g, int x, int y, int width, int height,
                           float sliderPosProportional, float rotaryStartAngle, float rotaryEndAngle,
                           juce::Slider& slider) override
    {
        juce::LookAndFeel_V2::drawRotarySlider(g, x, y, width, height, sliderPosProportional, rotaryStartAngle, rotaryEndAngle, slider);

        auto title = slider.getProperties()["title"].toString();

        auto bounds = slider.getLocalBounds();
        
        auto textColour = slider.findColour(juce::Slider::textBoxTextColourId);
        g.setColour(textColour);
        
        {
            const juce::Graphics::ScopedSaveState save (g);
            juce::GlyphArrangement arrangement;
            arrangement.addFittedText(g.getCurrentFont(), title, bounds.getX(), bounds.getY(), bounds.getWidth(), kDecoSpace, juce::Justification::centred, 1);
            g.excludeClipRegion(arrangement.getBoundingBox(0, title.length(), true).toNearestIntEdges().expanded(3, 3));
            
            g.drawRoundedRectangle(bounds.toFloat().reduced(10.0f), 10.0f, 3.0f);
        }
        
        g.drawFittedText (title, bounds.withHeight(kDecoSpace), juce::Justification::centred, 1);
    }
    
    juce::Slider::SliderLayout getSliderLayout (juce::Slider& slider) override
    {
        // 1. compute the actually visible textBox size from the slider textBox size and some additional constraints

        int minXSpace = 0;
        int minYSpace = 0;

        auto textBoxPos = slider.getTextBoxPosition();

        if (textBoxPos == juce::Slider::TextBoxLeft || textBoxPos == juce::Slider::TextBoxRight)
            minXSpace = 30;
        else
            minYSpace = 15;

        auto localBounds = slider.getLocalBounds().reduced(kDecoSpace); // <== Reduce the slider layout here to make space for your decorations

        auto textBoxWidth  = std::clamp(localBounds.getWidth() - minXSpace, 0, slider.getTextBoxWidth());
        auto textBoxHeight = std::clamp(localBounds.getHeight() - minYSpace, 0, slider.getTextBoxHeight());

        juce::Slider::SliderLayout layout;

        // 2. set the textBox bounds

        if (textBoxPos != juce::Slider::NoTextBox)
        {
            if (slider.isBar())
            {
                layout.textBoxBounds = localBounds;
            }
            else
            {
                layout.textBoxBounds.setWidth (textBoxWidth);
                layout.textBoxBounds.setHeight (textBoxHeight);

                if (textBoxPos == juce::Slider::TextBoxLeft)           layout.textBoxBounds.setX (localBounds.getX());
                else if (textBoxPos == juce::Slider::TextBoxRight)     layout.textBoxBounds.setX (localBounds.getRight() - textBoxWidth);
                else /* above or below -> centre horizontally */       layout.textBoxBounds.setX (localBounds.getCentreX() - textBoxWidth / 2);

                if (textBoxPos == juce::Slider::TextBoxAbove)          layout.textBoxBounds.setY (localBounds.getY());
                else if (textBoxPos == juce::Slider::TextBoxBelow)     layout.textBoxBounds.setY (localBounds.getBottom() - textBoxHeight);
                else /* left or right -> centre vertically */          layout.textBoxBounds.setY (localBounds.getCentreY() - textBoxHeight / 2);
            }
        }

        // 3. set the slider bounds

        layout.sliderBounds = localBounds;

        if (slider.isBar())
        {
            layout.sliderBounds.reduce (1, 1);   // bar border
        }
        else
        {
            if (textBoxPos == juce::Slider::TextBoxLeft)       layout.sliderBounds.removeFromLeft (textBoxWidth);
            else if (textBoxPos == juce::Slider::TextBoxRight) layout.sliderBounds.removeFromRight (textBoxWidth);
            else if (textBoxPos == juce::Slider::TextBoxAbove) layout.sliderBounds.removeFromTop (textBoxHeight);
            else if (textBoxPos == juce::Slider::TextBoxBelow) layout.sliderBounds.removeFromBottom (textBoxHeight);

            const int thumbIndent = getSliderThumbRadius (slider);

            if (slider.isHorizontal())    layout.sliderBounds.reduce (thumbIndent, 0);
            else if (slider.isVertical()) layout.sliderBounds.reduce (0, thumbIndent);
        }

        return layout;
    }
};


I think that with these complex solutions I always end up getting the kind of pain I was referring to in the first message.

I have built an application with the code to test how it worked. Initially it seems to work fine, although it is not what I want the control to shrink to fit, but ok, it works fine. The control displays the thumb, text and frame correctly.

a

Now I have made a change to hide the value with setTextBoxStyle, the result is that everything is messed up.

b

Surely this could be fixed, but it is not the architecture I want. It seems better to me what I did to create an alternative virtual paint function that allows to paint from the parent with the clipping of the parent. so I don’t have to worry about anything else, and my controls will always have the same position and size. And I still think that the developers should implement it, it doesn’t seem to have any inconvenience since the parent control is simply drawing on its own control using the paintDecorations method of each child.

I finally found a satisfactory solution that does not involve modifying Juce code. Use the paintOverChild function as a custom function that will be called by the container with its clipping, although using the coordinates of the child component

    for (auto* child : getChildren())
    {       
        g.saveState(); 
        g.addTransform(juce::AffineTransform::translation(child->getX(), child->getY()));
        g.setFont(1);
        child->paintOverChildren(g);
        g.restoreState();
    }

setFont(1) is the way to detect if the function was called from my scope (for the moment it is the best I found). so there is nothing to do but add the function on any desired component and paint as normal.

    void paintOverChildren(juce::Graphics& g) override
    { 
        if (g.getCurrentFont().getHeight()!=1) return;

        // paint with the parent cliping
    }

this is only valid for static drawings, for dynamic components like slider shadows I have to call repaint on the parent component at each value change.

it is not as ideal as I would like, but I think it is the best solution from the point of view of practicality and simplicity.