diff --git a/modules/juce_gui_basics/components/juce_Component.cpp b/modules/juce_gui_basics/components/juce_Component.cpp index e11740f2c5..6643d0355c 100644 --- a/modules/juce_gui_basics/components/juce_Component.cpp +++ b/modules/juce_gui_basics/components/juce_Component.cpp @@ -209,7 +209,7 @@ public: return std::exchange (effect, &i) != &i; } - void paint (Graphics& g, Component& c, bool ignoreAlphaLevel) + void paint (Graphics& g, Component& c, bool ignoreAlphaLevel, OpaqueLayer& opaqueLayer) { auto scale = g.getInternalContext().getPhysicalPixelScaleFactor(); auto scaledBounds = c.getLocalBounds() * scale; @@ -238,7 +238,7 @@ public: Graphics g2 (effectImage); g2.addTransform (AffineTransform::scale ((float) scaledBounds.getWidth() / (float) c.getWidth(), (float) scaledBounds.getHeight() / (float) c.getHeight())); - c.paintComponentAndChildren (g2); + c.paintComponentAndChildren (g2, opaqueLayer); } Graphics::ScopedSaveState ss (g); @@ -257,6 +257,183 @@ private: ImageEffectFilter* effect; }; +//============================================================================== +class Component::OpaqueLayer +{ +public: + explicit OpaqueLayer (const Component* c) + : currentComponent (c) + { + jassert (c != nullptr); + appendOpaqueChildren (*c, {}, c->getLocalBounds()); + } + + [[nodiscard]] auto pushComponent (Component& component) + { + removeOpaqueComponent (component); + currentComponent = &component; + + const auto pos = component.getPosition(); + offsetFromOrigin += pos; + + return ScopeGuard { [this, pos] + { + offsetFromOrigin -= pos; + currentComponent = nullptr; + } }; + } + + Rectangle getCurrentComponentBounds (const juce::Graphics& g) + { + // This function must only be called while the object returned by + // pushComponent() is still alive! + jassert (currentComponent != nullptr); + const auto pos = currentComponent->getPosition(); + const auto bounds = getNonOccludedBoundsForCurrentComponent (g.getClipBounds() - pos); + + if (! bounds.isEmpty()) + return bounds + pos; + + removeOpaqueChildren (*currentComponent); + return {}; + } + + Rectangle getCurrentComponentPaintBounds (const juce::Graphics& g) + { + // This function must only be called while the object returned by + // pushComponent() is still alive! + jassert (currentComponent != nullptr); + return getNonOccludedBoundsForCurrentComponent (g.getClipBounds()); + } + +private: + enum class ObscuredBy + { + childrenOnly, + allButChildren + }; + + template + Rectangle getNonOccludedBoundsForCurrentComponent (Rectangle clipBounds) const + { + auto visibleBounds = currentComponent->getLocalBounds().getIntersection (clipBounds); + + if (visibleBounds.isEmpty()) + return {}; + + RectangleList visibleRegions { visibleBounds }; + + const auto occlude = [&] (const OpaqueComponentInfo& opaqueComponentInfo) + { + const auto opaqueBounds = opaqueComponentInfo.clippedBounds - offsetFromOrigin; + + if (! opaqueBounds.intersects (visibleBounds)) + return; + + if (opaqueBounds.contains (visibleBounds)) + visibleRegions.clear(); + else + visibleRegions.subtract (opaqueBounds); + + visibleBounds = visibleRegions.getBounds(); + }; + + if constexpr (obscuredBy == ObscuredBy::childrenOnly) + { + for (auto i = currentPosition; i < opaqueComponents.size(); ++i) + { + const auto& opaqueComponentInfo = opaqueComponents.getReference (i); + + if (! currentComponent->isParentOf (opaqueComponentInfo.component)) + break; + + occlude (opaqueComponentInfo); + + if (visibleBounds.isEmpty()) + return {}; + } + } + else + { + for (int i = opaqueComponents.size(); --i >= currentPosition;) + { + const auto& opaqueComponentInfo = opaqueComponents.getReference (i); + + if (currentComponent->isParentOf (opaqueComponentInfo.component)) + break; + + occlude (opaqueComponentInfo); + + if (visibleBounds.isEmpty()) + return {}; + } + } + + return visibleBounds; + } + + void removeOpaqueComponent (const Component& component) + { + if (opaqueComponents[currentPosition].component == &component) + ++currentPosition; + } + + void removeOpaqueChildren (const Component& component) + { + for (const auto* child : component.getChildren()) + { + if (currentPosition == opaqueComponents.size()) + return; + + if (! isVisibleToLayer (*child)) + continue; + + removeOpaqueComponent (*child); + removeOpaqueChildren (*child); + } + } + + static bool isVisibleToLayer (const Component& c) + { + return detail::ComponentHelpers::isVisibleWithNonZeroArea (c) + && c.componentTransparency == 0 + && ! c.isTransformed(); + } + + void appendOpaqueChildren (const Component& parent, + Point parentOrigin, + Rectangle parentClippedBounds) + { + for (auto* child : parent.getChildren()) + { + if (! isVisibleToLayer (*child)) + continue; + + const auto childBounds = child->getBounds() + parentOrigin; + const auto childClippedBounds = parentClippedBounds.getIntersection (childBounds); + + if (childClippedBounds.isEmpty()) + continue; + + if (child->isOpaque()) + opaqueComponents.add ({ child, childClippedBounds }); + + appendOpaqueChildren (*child, childBounds.getPosition(), childClippedBounds); + } + } + + struct OpaqueComponentInfo + { + const Component* component; + Rectangle clippedBounds; + }; + + int currentPosition{}; + Point offsetFromOrigin{}; + const Component* currentComponent; + Array opaqueComponents; +}; + //============================================================================== Component::Component() noexcept : componentFlags (0) @@ -1701,17 +1878,17 @@ void Component::paintOverChildren (Graphics&) } //============================================================================== -void Component::paintWithinParentContext (Graphics& g) +void Component::paintWithinParentContext (Graphics& g, OpaqueLayer& opaqueLayer) { g.setOrigin (getPosition()); if (cachedImage != nullptr) cachedImage->paint (g); else - paintEntireComponent (g, false); + paintEntireComponent (g, false, opaqueLayer); } -void Component::paintComponentAndChildren (Graphics& g) +void Component::paintComponentAndChildren (Graphics& g, OpaqueLayer& opaqueLayer) { #if JUCE_ETW_TRACELOGGING { @@ -1727,70 +1904,64 @@ void Component::paintComponentAndChildren (Graphics& g) } #endif - auto clipBounds = g.getClipBounds(); + const auto paintBounds = opaqueLayer.getCurrentComponentPaintBounds (g); - if (flags.dontClipGraphicsFlag && getNumChildComponents() == 0) - { - paint (g); - } - else + if (! paintBounds.isEmpty()) { Graphics::ScopedSaveState ss (g); - if (! (detail::ComponentHelpers::clipObscuredRegions (*this, g, clipBounds, {}) && g.isClipEmpty())) - paint (g); + if (! isPaintingUnclipped()) + g.reduceClipRegion (paintBounds); + + paint (g); } - for (int i = 0; i < childComponentList.size(); ++i) + for (auto* child : getChildren()) { - auto& child = *childComponentList.getUnchecked (i); + if (! detail::ComponentHelpers::isVisibleWithNonZeroArea (*child)) + continue; - if (child.isVisible()) + if (child->isTransformed() || child->componentTransparency != 0) { - if (child.affineTransform != nullptr) - { - Graphics::ScopedSaveState ss (g); + Graphics::ScopedSaveState ss (g); - g.addTransform (*child.affineTransform); + if (auto& transform = child->affineTransform) + g.addTransform (*transform); - if ((child.flags.dontClipGraphicsFlag && ! g.isClipEmpty()) || g.reduceClipRegion (child.getBounds())) - child.paintWithinParentContext (g); - } - else if (clipBounds.intersects (child.getBounds())) - { - Graphics::ScopedSaveState ss (g); + child->paintWithinParentContext (g, opaqueLayer); + } + else + { + const auto componentPopper = opaqueLayer.pushComponent (*child); + const auto componentBounds = opaqueLayer.getCurrentComponentBounds (g); - if (child.flags.dontClipGraphicsFlag) - { - child.paintWithinParentContext (g); - } - else if (g.reduceClipRegion (child.getBounds())) - { - bool nothingClipped = true; + if (componentBounds.isEmpty()) + continue; - for (int j = i + 1; j < childComponentList.size(); ++j) - { - auto& sibling = *childComponentList.getUnchecked (j); + Graphics::ScopedSaveState ss (g); - if (sibling.flags.opaqueFlag && sibling.isVisible() && sibling.affineTransform == nullptr) - { - nothingClipped = false; - g.excludeClipRegion (sibling.getBounds()); - } - } + if (! child->isPaintingUnclipped()) + g.reduceClipRegion (componentBounds); - if (nothingClipped || ! g.isClipEmpty()) - child.paintWithinParentContext (g); - } - } + child->paintWithinParentContext (g, opaqueLayer); } } Graphics::ScopedSaveState ss (g); + + if (! isPaintingUnclipped()) + g.reduceClipRegion (getLocalBounds()); + paintOverChildren (g); } void Component::paintEntireComponent (Graphics& g, bool ignoreAlphaLevel) +{ + OpaqueLayer opaqueLayer { this }; + paintEntireComponent (g, ignoreAlphaLevel, opaqueLayer); +} + +void Component::paintEntireComponent (Graphics& g, bool ignoreAlphaLevel, OpaqueLayer& opaqueLayer) { // If sizing a top-level-window and the OS paint message is delivered synchronously // before resized() is called, then we'll invoke the callback here, to make sure @@ -1806,20 +1977,26 @@ void Component::paintEntireComponent (Graphics& g, bool ignoreAlphaLevel) if (effectState != nullptr) { - effectState->paint (g, *this, ignoreAlphaLevel); + effectState->paint (g, *this, ignoreAlphaLevel, opaqueLayer); } else if (componentTransparency > 0 && ! ignoreAlphaLevel) { if (componentTransparency < 255) { + OpaqueLayer transparentOpaqueLayer { this }; g.beginTransparencyLayer (getAlpha()); - paintComponentAndChildren (g); + paintComponentAndChildren (g, transparentOpaqueLayer); g.endTransparencyLayer(); } } + else if (isTransformed()) + { + OpaqueLayer transformedOpaqueLayer { this }; + paintComponentAndChildren (g, transformedOpaqueLayer); + } else { - paintComponentAndChildren (g); + paintComponentAndChildren (g, opaqueLayer); } #if JUCE_DEBUG @@ -3090,7 +3267,13 @@ struct ComponentTests : public UnitTest ++numPaintCalls; } + void paintOverChildren (Graphics&) final + { + ++numPaintOverChildrenCalls; + } + int numPaintCalls = 0; + int numPaintOverChildrenCalls = 0; Rectangle lastClipBounds; }; @@ -3116,12 +3299,16 @@ struct ComponentTests : public UnitTest parent->addAndMakeVisible (*child); expectEquals (parent->numPaintCalls, 0); + expectEquals (parent->numPaintOverChildrenCalls, 0); expectEquals (child->numPaintCalls, 0); + expectEquals (child->numPaintOverChildrenCalls, 0); paintComponentBounds (*parent); expectEquals (parent->numPaintCalls, 1); + expectEquals (parent->numPaintOverChildrenCalls, 1); expectEquals (child->numPaintCalls, 1); + expectEquals (child->numPaintOverChildrenCalls, 1); } beginTest ("Non-opaque children require their parent to repaint"); @@ -3157,6 +3344,7 @@ struct ComponentTests : public UnitTest expectEquals (parent->numPaintCalls, 0); expectEquals (child->numPaintCalls, 1); + expectEquals (parent->numPaintOverChildrenCalls, 1); } beginTest ("Opaque children don't require their parent to repaint (even when the parent uses setPaintingIsUnclipped (true))"); @@ -3176,6 +3364,7 @@ struct ComponentTests : public UnitTest expectEquals (parent->numPaintCalls, 0); expectEquals (child->numPaintCalls, 1); + expectEquals (parent->numPaintOverChildrenCalls, 1); } beginTest ("A partially obscured parent will repaint with reduced clip bounds"); @@ -3218,6 +3407,7 @@ struct ComponentTests : public UnitTest paintComponentBounds (*parent); expectEquals (parent->numPaintCalls, 0); + expectEquals (parent->numPaintOverChildrenCalls, 1); expectEquals (child1->numPaintCalls, 1); expectEquals (child2->numPaintCalls, 1); } @@ -3245,11 +3435,44 @@ struct ComponentTests : public UnitTest paintComponentBounds (*parent); expectEquals (parent->numPaintCalls, 0); + expectEquals (parent->numPaintOverChildrenCalls, 1); expectEquals (child1->numPaintCalls, 0); + expectEquals (child1->numPaintOverChildrenCalls, 0); expectEquals (child2->numPaintCalls, 1); expectEquals (child3->numPaintCalls, 1); } + beginTest ("An opaque component will hide parent-sibling components behind it"); + { + const auto parent = std::make_unique(); + const auto child1 = std::make_unique(); + const auto child2 = std::make_unique(); + const auto child3 = std::make_unique(); + + Rectangle bounds { 0, 0, 100, 100 }; + parent->setBounds (bounds); + + child1->setBounds (bounds); + parent->addAndMakeVisible (*child1); + + child2->setBounds (bounds); + parent->addAndMakeVisible (*child2); + + child3->setBounds (bounds); + child3->setOpaque (true); + child2->addAndMakeVisible (*child3); + + paintComponentBounds (*parent); + + expectEquals (parent->numPaintCalls, 0); + expectEquals (parent->numPaintOverChildrenCalls, 1); + expectEquals (child1->numPaintCalls, 0); + expectEquals (child1->numPaintOverChildrenCalls, 0); + expectEquals (child2->numPaintCalls, 0); + expectEquals (child2->numPaintOverChildrenCalls, 1); + expectEquals (child3->numPaintCalls, 1); + } + beginTest ("An opaque component will reduce the clip bounds of sibling components behind it"); { const auto parent = std::make_unique(); @@ -3427,7 +3650,7 @@ struct ComponentTests : public UnitTest expectEquals (grandchild->numPaintCalls, 0); } - beginTest ("Components with a width of 0 will not have their paint function called"); + beginTest ("Components with a width of 0 will not have their paint functions called"); { const auto parent = std::make_unique(); const auto child = std::make_unique(); @@ -3442,9 +3665,10 @@ struct ComponentTests : public UnitTest expectEquals (parent->numPaintCalls, 1); expectEquals (child->numPaintCalls, 0); + expectEquals (child->numPaintOverChildrenCalls, 0); } - beginTest ("Components with a height of 0 will not have their paint function called"); + beginTest ("Components with a height of 0 will not have their paint functions called"); { const auto parent = std::make_unique(); const auto child = std::make_unique(); @@ -3459,6 +3683,7 @@ struct ComponentTests : public UnitTest expectEquals (parent->numPaintCalls, 1); expectEquals (child->numPaintCalls, 0); + expectEquals (child->numPaintOverChildrenCalls, 0); } beginTest ("Transparent components will not be considered opaque"); @@ -3480,6 +3705,30 @@ struct ComponentTests : public UnitTest expectEquals (child->numPaintCalls, 1); } + beginTest ("Opaque components will only be considered opaque up to a transparent parent"); + { + const auto parent = std::make_unique(); + const auto child1 = std::make_unique(); + const auto child2 = std::make_unique(); + + Rectangle bounds { 0, 0, 100, 100 }; + parent->setBounds (bounds); + + child1->setBounds (bounds); + child1->setAlpha (0.5f); + parent->addAndMakeVisible (*child1); + + child2->setBounds (bounds); + child2->setOpaque (true); + child1->addAndMakeVisible (*child2); + + paintComponentBounds (*parent); + + expectEquals (parent->numPaintCalls, 1); + expectEquals (child1->numPaintCalls, 0); + expectEquals (child2->numPaintCalls, 1); + } + beginTest ("Transformed components will not be considered opaque"); { const auto parent = std::make_unique(); @@ -3499,6 +3748,30 @@ struct ComponentTests : public UnitTest expectEquals (child->numPaintCalls, 1); } + beginTest ("Opaque components will only be considered opaque up to a transformed parent"); + { + const auto parent = std::make_unique(); + const auto child1 = std::make_unique(); + const auto child2 = std::make_unique(); + + Rectangle bounds { 0, 0, 100, 100 }; + parent->setBounds (bounds); + + child1->setBounds (bounds); + child1->setTransform (AffineTransform::rotation (degreesToRadians (45.0f))); + parent->addAndMakeVisible (*child1); + + child2->setBounds (bounds); + child2->setOpaque (true); + child1->addAndMakeVisible (*child2); + + paintComponentBounds (*parent); + + expectEquals (parent->numPaintCalls, 1); + expectEquals (child1->numPaintCalls, 0); + expectEquals (child2->numPaintCalls, 1); + } + beginTest ("Nested opaque components prevent parents from being painted"); { const auto parent = std::make_unique(); @@ -3522,6 +3795,31 @@ struct ComponentTests : public UnitTest expectEquals (child1->numPaintCalls, 0); expectEquals (child2->numPaintCalls, 1); } + + beginTest ("Areas of an opaque component outside its parent will not be considered opaque"); + { + const auto parent = std::make_unique(); + const auto child1 = std::make_unique(); + const auto child2 = std::make_unique(); + const auto child3 = std::make_unique(); + + parent->setBounds ({ 0, 0, 100, 100 }); + + child1->setBounds ({ 50, 0, 50, 100 }); + parent->addAndMakeVisible (*child1); + + child2->setBounds ({ -50, 0, 100, 100 }); + child2->setOpaque (true); + child1->addAndMakeVisible (*child2); + + paintComponentBounds (*parent); + + expectEquals (parent->numPaintCalls, 1); + expect (parent->lastClipBounds == Rectangle { 0, 0, 50, 100 }); + expectEquals (child1->numPaintCalls, 0); + expectEquals (child2->numPaintCalls, 1); + expect (child2->lastClipBounds == Rectangle { 50, 0, 50, 100 }); + } } }; diff --git a/modules/juce_gui_basics/components/juce_Component.h b/modules/juce_gui_basics/components/juce_Component.h index 3944cac82a..02dfada419 100644 --- a/modules/juce_gui_basics/components/juce_Component.h +++ b/modules/juce_gui_basics/components/juce_Component.h @@ -1194,7 +1194,7 @@ public: If you enable this mode, you'll need to make sure your paint method doesn't call anything like Graphics::fillAll(), and doesn't draw beyond the component's bounds, because that'll produce - artifacts. This option will have no effect on components that contain any child components. + artifacts. */ void setPaintingIsUnclipped (bool shouldPaintWithoutClipping) noexcept; @@ -1276,14 +1276,20 @@ public: /** Indicates whether any parts of the component might be transparent. Components that always paint all of their contents with solid colour and - thus completely cover any components behind them should use this method + thus completely cover any components behind them, can use this method to to tell the repaint system that they are opaque. This information is used to optimise drawing, because it means that - objects underneath opaque windows don't need to be painted. + objects underneath opaque components or windows don't need to be painted + or can have their clip bounds reduced to a smaller size. - By default, components are considered transparent, unless this is used to - make it otherwise. + Note however that there is a cost for every other component to check if + it is being obscured by opaque components. This cost should be carefully + weighed up against the benefits before deciding to enable this. + + The default value of this property is false, which means that a + component will be considered transparent unless setOpaque (true) is + called on that component. @see isOpaque */ @@ -2712,6 +2718,8 @@ private: uint8 componentTransparency = 0; //============================================================================== + class OpaqueLayer; + static void internalMouseEnter (SafePointer, MouseInputSource, Point, Time); static void internalMouseExit (SafePointer, MouseInputSource, Point, Time); static void internalMouseDown (SafePointer, MouseInputSource, const detail::PointerState&, Time); @@ -2733,8 +2741,9 @@ private: void internalRepaintUnchecked (Rectangle, bool); Component* removeChildComponent (int index, bool sendParentEvents, bool sendChildEvents); void reorderChildInternal (int sourceIndex, int destIndex); - void paintComponentAndChildren (Graphics&); - void paintWithinParentContext (Graphics&); + void paintEntireComponent (Graphics&, bool, OpaqueLayer&); + void paintComponentAndChildren (Graphics&, OpaqueLayer&); + void paintWithinParentContext (Graphics&, OpaqueLayer&); void sendMovedResizedMessages (bool wasMoved, bool wasResized); void sendMovedResizedMessagesIfPending(); void repaintParent(); diff --git a/modules/juce_gui_basics/detail/juce_ComponentHelpers.h b/modules/juce_gui_basics/detail/juce_ComponentHelpers.h index 2c7f44d670..948cda0378 100644 --- a/modules/juce_gui_basics/detail/juce_ComponentHelpers.h +++ b/modules/juce_gui_basics/detail/juce_ComponentHelpers.h @@ -193,42 +193,6 @@ struct ComponentHelpers return convertFromDistantParentSpace (topLevelComp, *target, p); } - static bool clipChildComponent (const Component& child, - Graphics& g, - const Rectangle clipRect, - Point delta) - { - if (! child.isVisible() || child.isTransformed()) - return false; - - const auto newClip = clipRect.getIntersection (child.boundsRelativeToParent); - - if (newClip.isEmpty()) - return false; - - if (child.isOpaque() && child.componentTransparency == 0) - { - g.excludeClipRegion (newClip + delta); - return true; - } - - const auto childPos = child.getPosition(); - return clipObscuredRegions (child, g, newClip - childPos, childPos + delta); - } - - static bool clipObscuredRegions (const Component& comp, - Graphics& g, - const Rectangle clipRect, - Point delta) - { - auto wasClipped = false; - - for (int i = comp.childComponentList.size(); --i >= 0;) - wasClipped |= clipChildComponent (*comp.childComponentList.getUnchecked (i), g, clipRect, delta); - - return wasClipped; - } - static Rectangle getParentOrMainMonitorBounds (const Component& comp) { if (auto* p = comp.getParentComponent()) @@ -263,6 +227,13 @@ struct ComponentHelpers function (c, ms, SH::screenPosToLocalPos (*c, ms.getScreenPosition()), Time::getCurrentTime()); } + static bool isVisibleWithNonZeroArea (const Component& component) + { + return component.isVisible() + && component.getWidth() > 0 + && component.getHeight() > 0; + } + class ModalComponentManagerChangeNotifier { public: