TextEditor painting jank

Has anyone else ran into characters painting jankily in the TextEditor when using custom typefaces? I haven’t checked outside of MacOS on latest JUCE.

This is happening almost every letter, first paint call has a fragment:

Second paint call “finishes” the letter or group of letters:

Typing text feels a bit delayed / lofi / rough / bouncy as a result…

(I also hear an audio glitch on my very first keystroke after the app starts, which makes me wonder if Something Bad is happening with a KeyListener somewhere…)

Just a note to say I reproduced this issue with the system font, but can’t yet reproduce on a fresh project with system or custom typeface.

Revisiting this…just a note to say the jank was caused by a cascade of extra repainting between letter entries, triggered by CaretComponent constantly changing visibilities. I’m aiming to resolve by customizing CaretComponent to not repaint the world (or figure out how to cut off the bleeding a bit higher up the component hierarchy)

1 Like

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:

I may have misunderstood but I think this relates to what I’ve said this elsewhere in this forum. If you want the most optimal performance (especially when using `setBufferedToImage) painting should be restricted to leaf node components only. That is to say a component should either…

  1. Have child components
  2. Implement a paint method

A component should never do both!

So I would suggest the following tree…

└─ Green Container
   └─ Green Component
└─ Blue Modal Container
   └─ Blue Component
   └─ TextEditor Container
      └─ TextEditor
      └─ CaretComponent  // I realise this probably isn't possible to be a sibling?

Hey Anthony,

Thanks for weighing in! I referenced some of your forum posts when reading through the component code and think that it’s generally good advice. It might be nice to hear you expand on the “why,” if you could…

Unfortunately I don’t think your suggested hierarchy changes the particular problem in this thread. The green container/component will always paint on cursor blink, even if setOpaque is set on the TextEditor. This is because at the point where the green/blue containers are siblings, there’s no recursion into the children to understand if anything is opaque.

It’s true that setBufferedToImage (true) will mean a cached image will be painted instead of the component’s paint method and children. But, as Jimmi pointed out, the entire cached image will be painted (not just the cursor’s clip bounds).

Here’s the minimal sibling example I’ve been using: TextEditor, Cursor, and setOpaque · GitHub

Well…

…that’s the why.

It’s completely possible there is an optimisation that is being missed. However, I recall discussing it with Jules years ago, and there being a reason that a particular optimisation couldn’t be done. Maybe related to some other edge case that needed to be covered? I can’t remember to be honest. I think the discussion is somewhere on the forums.

1 Like

This is the conversation I recall having with Jules (5 years and 16 days ago!).

This is the conversation I recall having with Jules

Ah yes, I read this (and every other post on component painting!) :laughing:

Jules’ reply there implies the setOpaque child case wasn’t handled:

Having nested, buffered, opaque components that aren’t perfectly efficiently handled could just be an edge-case that was overlooked, or where it just wasn’t worth the extra step of checking for occlusion just to handle a rare situation.

Having a TextEditor on a modal displaying over other UI elements doesn’t seem like it would be too rare, so maybe there’s an opportunity for improvement…

To try to answer my own question:

I would describe the technique of painting only on leaf nodes :leaves:with heavy usage of setBufferedToImage (true) as enabling what’s often called “Russian Doll caching.” Increasing the amount of branching allows smaller parts of the UI to “expire” (have their paint method called) — instead of having one mega component expire that has to repaint itself and all its children.:nesting_dolls:

I like it!

To be clear I only recommend setBufferedToImage (true) if the the component meet both of these requirements…

  1. Is expensive to draw than an image
  2. Isn’t regularly redrawn anyway (like a meter)

So where I found this really helps is background components that might have enough detail in them that they are more expensive to draw than an image.

I like it!

:dancer:

if the the component meet both of these requirements…

With you on that! I’m working on a “how juce component painting works” guide, and I use drawing simple rectangles or painting every frame as examples of bad candidates.

2 Likes