TextEditor painting jank

After more investigation, this issue boiled down to how JUCE painting is optimized.

Take the following:

…with the following component hierarchy:

Editor
├─ Green Component
└─ Blue Modal
   └─ TextEditor
      └─ CaretComponent  

When the CarentComponent blinks, it invalidates a 2px portion of the screen (for example with bounds 117 59 2 63).

The blue and green components here are siblings. As a paint call filters down the component tree with these clip bounds, it looks the siblings, asking if they are opaque. If the modal is opaque, it clips out the modal from the clip bounds and the green component ends up with empty clip bounds (and not painting).

If the modal is transparent, both components end up with a paint call for that 2px portion. This is because at this point, the painting logic doesn’t check children of siblings — so the TextEditor being opaque has no effect on anything in the green component’s “branch” of the tree.

If your UI needs a transparent modal for Reasons (rounded corners, actual transparency), making it a child instead of a sibling improves things:

└─ Green Component
   └─ Blue Modal
      └─ TextEditor
         └─ CaretComponent  

This is because clipObscuredRegions recursively traverses the children and eventually finds and removes the TextEditor region (as it’s setOpaque(true)), preventing the main component AND modal from being repainted — just what we want!

Bottom line

For setOpaque (true) to do its job (prevent paint calls), the components occupying the same bounds (i.e. “behind” the component) must be direct ancestors (parents, parents of parents, etc) or direct siblings. They can’t be a sibling of an ancestor.

In other words: Your Aunties and Uncles don’t care how opaque you are. :sunglasses:

Takeaway

Composing components in direct parent->child relationships best takes advantage of JUCE’s bounds clipping paint optimizations.

2 downsides here:

  1. The screen location doesn’t necessarily correlate with semantic function. For example, a preset browser has nothing to with a filter section it might overlay, so nesting these makes no sense.

  2. A component might overlay several other components in different parts of the hierarchy…which is just awkward…

Potential framework improvement?

An alternative setOpaque implementation: When true, add the component to a list called alwaysClipComponents. At the top of a paint call, check components in this list for isShowing(), and if true, clip these bounds out of the graphics context. This would replace the expensive recursion overhead in clipObscuredRegions and improve setOpaque's “coverage” so it doesn’t have exceptions.

Workaround

setBufferedToImage (true) on the Uncle and Auntie components reduces the work happening on something like a caret repaint. Imperfect, as they shouldn’t paint to begin with, but functional.

Terrible hack

In Uncle and Auntie paint methods (and their children), if g.getClipBounds() is 2px wide, do an early return :rofl: