Any way to clip the entire graphics region of a component and its children?

For example, I have a custom ListBox implementation that I want to have rounded corners. It’s easy enough to draw the background of this component with rounded borders, but there’s no way I can find to make the child ListBox cells and accompanying scrollbar obey this roundness as well. Ideally I’m looking for something like Graphics::reduceClipRegion(juce::Path) but for an entire component and its children to obey. i.e. Component::reduceClipRegion

I find this to be a common problem with Juce and am often able to hack something like this by manually clipping a component’s paint routine and any children that might lie in the perceived clip region. But that’s not an option here given that all of the cells are a moving target.

3 Likes

I’m very interested in this issue. I thought about it a while ago and I was wondering if you could use a “cover” component for this,which is transparent except for the corners?

1 Like

I’ve done this before as well and it will work fine in cases that have a solid or simple background. My particular case also has a drop shadow (another thing Juce Graphics has issues with) behind my component which makes this solution kind of impossible. It also just feels unfortunate to have to hack a solution like this just to implement something as common as rounded corners.

If you have a simple background color, this function draws the rounded corners over your child component. It’s a hack, but it works.

void YourComponent::paintOverChildren(juce::Graphics &g)
{
    auto backgroundColour = juce::Colour(0xffff0000);
    auto size = 20;
    auto area = getLocalBounds ();
    g.setColour(backgroundColour);
    juce::Path topLeft;

    topLeft.addArc (area.getX(),area.getY(), size, size
              , juce::MathConstants<float>::pi * 1.5
              , juce::MathConstants<float>::pi * 2
              , true);
    topLeft.lineTo (area.getX(),area.getY());
    topLeft.closeSubPath ();
    g.fillPath (topLeft);

    juce::Path topRight;
    topRight.addArc (
                area.getWidth () - size
              , area.getY ()
              , size, size
              , juce::MathConstants<float>::pi * 2
              , juce::MathConstants<float>::pi * 2.5
              , true);
    topRight.lineTo (area.getWidth (), area.getY ());
    topRight.closeSubPath ();
    g.fillPath (topRight);

    juce::Path bottomRight;
    bottomRight.addArc (area.getWidth () - size, area.getHeight () - size, size, size
              , juce::MathConstants<float>::pi * 2.5
              , juce::MathConstants<float>::pi * 3
              , true);
    bottomRight.lineTo (area.getWidth () , area.getHeight () );
    bottomRight.closeSubPath ();
    g.fillPath (bottomRight);

    juce::Path bottomLeft;
    bottomLeft.addArc (area.getX (), area.getHeight () - size, size, size
              , juce::MathConstants<float>::pi * 3
              , juce::MathConstants<float>::pi * 3.5
              , true);
    bottomLeft.lineTo (area.getX (), area.getHeight ());
    bottomLeft.closeSubPath ();
    g.fillPath (bottomLeft);
}

Nice! Even simpler would be:

Path fakeRoundedCorners;
juce::Rectangle<float> bounds = {100, 100, 200, 200}; //your component's bounds

const float cornerSize = 10.f; //desired corner size
fakeRoundedCorners.addRectangle(bounds); //What you start with
fakeRoundedCorners.setUsingNonZeroWinding(false); //The secret sauce
fakeRoundedCorners.addRoundedRectangle(bounds, cornerSize); //subtract this shape

g.setColour(Colours::green);
g.fillPath(fakeRoundedCorners);

This gives you:

But unfortunately it’s not something I can use so we just opted to get rid of the rounded corners until such time that juce can accommodate something like that.

5 Likes

I’d like to bump this as it doesn’t seem it would an extremely heavy lift and would add a lot of functionality to JUCE for rounded corners which is a large use case where the OS design is going on. (They now round the corner on all windows on Mac OS)

similar to setPaintingIsUnclipped() – maybe we could have a:

setClippingRegionForChildren(bool shouldClipChildPainting, Path clipPath);

This would then set a flag used in:

void Component::paintWithinParentContext (Graphics& g)

We could traverse the parent components and check for the first one which has painting clipped for children, we could just set the clip region to the parent – of course like painting is unclipped you wouldn’t want to use fillAll etc – but then we could have rounded parent components with children? Would make for a much easier case when dealing with rounded corners!

With the way things are going maybe we should just be able to set the corner radius on components at this point :sweat_smile:

1 Like

This would be major. +1

Has anyone found a good method for doing this? I am facing exactly the same situation: a parent with rounded corners that dynamically draws children within its bounds.

I would do something like this->

static void setAsCornerOwner(juce::Component& c, float roundness)
{
  c.getProperties().set("corner_owner",true);
  c.getProperties().set("roundness",roundness);
}


// 
juce::Component myCornerOwner;
setAsCornerOwner(myCornerOwner,10);


static juce::Path getClip(juce::Component* c)
 {
juce::Path clip;

  juce::Component* corner_candidate =  c;

bool found = false;

  while(corner_candidate )
  {
    if(corner_candidate ->getPropertries().contains("corner_owner"))
    {
   found = true;
break;
    }
  corner_candidate = corner_candidate .getParentComponent();
   }

if(found)
{
clip.addRoundedRectangle(corner_candidate.getLocalBounds()...
clip.addTransform(// move the clip according to the requester's position, sorry, can't come up with it just like that
}

return clip;
}

(your paint method)

void paint(juce::Graphics& g)
{
g.clipRegion(getClip(this)...
}

it looks rough and it’s surely untested, but I believe this is the approach I would use, well, maybe get the clip at resize instead of repaint, but that’s basically it

1 Like

Would love this.

Bump!

Ok, I’ve made an implementation of this, I’m not sure it covers all cases but it works as far as I could test it.
Based on @nfect’s answer and implemented in the simplest, most intuitive way I could think of.

First, define these two functions:

inline std::unique_ptr<juce::Path> getRoundedCornersClipPath(juce::Component* comp) {
    auto* container = comp;
    while (container) {
        if (auto radius = container->getProperties().getVarPointer("corner-radius")) {
            auto path = std::make_unique<juce::Path>();
            path->addRoundedRectangle(comp->getLocalArea(container, container->getLocalBounds()), (float)*radius);
            return path;
        }
        container = container->getParentComponent();
    }
    return {};
}

inline void setRoundedCorners(juce::Component* comp, float radius) {
    comp->getProperties().set("corner-radius", radius);
}

Then, add a member variable to any component classes you want to be “clippable”

std::unique_ptr<juce::Path> clipPath;

Next, the path variable should be updated both in resized() and parentHierarchyChanged() - to cover Viewport usage scenarios and such… (and maybe also in moved())

void parentHierarchyChanged() override {
    ...
    clipPath = getRoundedCornersClipPath(this);
}

void resized() override {
    ...
    clipPath = getRoundedCornersClipPath(this);
}

And finally, clip that sh*t!

void paint(juce::Graphics& g) override {
    if (clipPath && getLocalBounds().toFloat().intersects(clipPath->getBounds()))
        g.reduceClipRegion(*clipPath);
    ...
}

Now “all” you have to do is set the corner radius on the constructor (or whatever suits you) of your container/parent:

class MyComponent : public juce::Component {
public:
    MyComponent() {
        ...
        setRoundedCorners(this, 20.0f);
    }
};

Hope this helps!

1 Like

Bump.

Ran into this problem so often already, a good solution by the JUCE Team would save so much time…