Background colour of TextEditor affects its opacity


#1

Why is TextEditor setting itself as opaque by default (setOpaque(true) at the beginning of its constructor), and again it adjusts its opacity every time the background colour changes?

There is a post where @jules states something about it (it’s from 2012, but that code hasn’t changed since):

There seems to be a wrong assumption there: if the look and feel renders the TextEditor with rounded corners, then its background colour may very well be fully opaque, but still the Component needs not to be marked opaque, because the area outside the rounded corners within the Component’s bounds should be transparent to whatever lies underneath.

It’s not just a matter of colour, but of shape as well.

Can this be fixed?


#2

Hi yfede,

This seems like a bit of an edge-case to change the TextEditor methods for but to get around this you could set your TextEditor’s colour to something transparent in the setColour() method and then when you draw the background in your custom LookAndFeel’s fillTextEditorBackground() method, draw the TextEditor box with the colour that you want. Then your TextEditor will be rendered properly and will be transparent to whatever is underneath.

Ed


#3

That’s exactly what I’m doing already, but you’ll agree with me that it is a very convoluted way to obtain something that should instead be as simple as setting the background colour of the editor to a fully opaque colour.

For example, Labels are very similar to TextEditors, but they (thank god!) don’t have this “automatic” setOpaque() feature and thus they can be rendered by the look and feel with whatever shape they want.

So my questions are:

Why is it so important to have a TextEditor set to be opaque whenever possible?

Why can’t it be set as non-opaque by default (which is the expected behavior by the way, because it’s also the default for all other widgets), and then the client code will turn it opaque if it really needs it to be?

I don’t think this is really only a corner-case (pun intended): almost all of the widgets here on mac where one can type into (think of search boxes), have some roundness in the corners that would not be possible with a fully opaque component. I am more of the opinion that every one encountering this before has worked around it like above and went onward. (and therefore the majority of the TextEditors out there should be non-opaque already, so making that their default condition should not hinder the performance of existing code at all)


#4

Hi yfede,

All good points but the main issue is that this change will cause performance degradation for people who have their TextEditor set to a solid background colour and expect it to not be painting anything behind it. I know the workaround is a bit of a faff but like you said, a lot of people have worked around it and moved onward.

Ed


#5

Really? How frequently is a TextEditor repainted?

I suppose it gets repainted at every keystroke AND at every blink of the caret, but in my opinion that’s still far less frequently than the repainting that happens, for example, while dragging a Slider.

My point is, that in the case of the Slider this auto opaque behavior is not present (probably because it is inherently non-opaque in the default Look and Feel), and still there haven’t been any performance complaints due to the dragging of non-opaque sliders.

My impression is that this auto opaque feature falls in the category of “premature optimization” that is not really needed, and it is not even documented anywhere in the TextEditor page.

And if I seem too strongly opinionated about this issue, it is because:

  1. I am very satisfied with the elegance of JUCE and this part really does not fit with the rest. It looks inconsistent.
  2. I have been bitten too many times by this: I regularly happen to draw rounded TextEditors in products I work on, they often have an opaque background colour, and every time I see the corners outside the outline being filled with garbage or just black. After a moment of puzzlement, I remember of this “thing” and put in place the workaround described above, again and again. After some iterations like that over the years, I thought it’s time to do something about it instead of “working around it and moving onward”

#6

I totally agree with yfede here. this is a bug and it should be fixed.

the following line in TextEditor::colourChanged() :

setOpaque (findColour (backgroundColourId).isOpaque());

makes the wrong assumption that the textEditor is fully painted with that backgroundColourId colour, without any rounded corners. This is a wrong assumption, as fillTextEditorBackground() is a customizable lookandfeel method.

To fix that, either setOpaque (findColour (backgroundColourId).isOpaque()); should be removed, either the following should be added at the beginning if TextEditor::paint() :

g.fillAll (findColour (TextEditor::backgroundColourId));

But at the moment, if you paint your textEditor with rounded corner :

void fillTextEditorBackground (Graphics& g, int w, int h, TextEditor& te) override
{
    g.setColour (te.findColour (TextEditor::backgroundColourId));
    float corner = 10.f;
    g.fillRoundedRectangle (0.f, 0.f, (float) w, (float) h, corner);
}

that will result in un-painted black background corners. Everybody will loose time debugging and doing a workaround the first time they encounter that bug.

edit : Simple code to reproduce the issue :

// MainComponent.h

#pragma once
#include "../JuceLibraryCode/JuceHeader.h"

struct MyLookAndFeel : public LookAndFeel_V4
{
    MyLookAndFeel()
    {
        setColour (TextEditor::outlineColourId, Colours::transparentWhite);
        setColour (TextEditor::focusedOutlineColourId, Colours::transparentWhite);
        setColour (TextEditor::backgroundColourId, Colours::red);
    }

    void fillTextEditorBackground (Graphics& g, int w, int h, TextEditor& te) override
    {
        g.setColour (findColour (TextEditor::backgroundColourId));
        float corner = 10.f;
        g.fillRoundedRectangle (0.f, 0.f, (float) w, (float) h, corner);
    }
};

//==============================================================================
class MainContentComponent   : public Component
{
public:
    //==============================================================================
    MainContentComponent()
    {
        textEditor.setLookAndFeel (&lnf);
        textEditor.setOpaque (false);
        addAndMakeVisible (textEditor);
        setSize (600, 400);
    }

    void paint (Graphics& g) override
    {
        g.fillAll (Colours::lightgrey);
    }

    void resized() override
    {
        textEditor.setBounds (getLocalBounds().reduced (10).removeFromTop (20));
    }

private:
    //==============================================================================
    MyLookAndFeel lnf;
    TextEditor textEditor;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainContentComponent)
};

#7

I think it makes sense to assume that the basic TextEditors drawn by JUCE’s L&F classes are fully opaque. This means that when they are drawn there is no need to re-draw what’s behind them or do any alpha-blending, which could be quite costly if you have lots of TextEditors. If you’re using a custom L&F to draw your TextEditor with rounded corners it should be up to you to deal with the opacity. So, like I mentioned earlier in the thread, you would set the background colour to something transparent and then in the fillTextEditorBackground() method you would draw your TextEditor with rounded corners and the desired colour.

I’d like to discuss this with Jules though, which will need to wait until he’s back from holiday so I’ll get back to you.


#8

That may be true for the specific case of the JUCE LnF classes, but that is incidental because all of them draw perfectly rectangular text editors.

It breaks the contract that changing the look and feel suffices to change the appearance of the GUI. As stated in the doc:

LookAndFeel objects define the appearance of all the JUCE widgets, and subclasses
can be used to apply different ‘skins’ to the application.

That’s not really true, if one needs to tamper with the opacity or colours of the TextEditor after the look and feel has been changed.

I also wonder why such care is taken about the performance of drawing text editors in particular: most GUIs out there are filled with other widgets, like Buttons or Sliders, which don’t cause noticeable performance degradation despite their non-rectangular shapes and non-opaque backgrounds.

Is that because you are afraid of complete UI redraws when the caret blinks? If that where problematic, imagine what would happen during a slider drag operation, which causes several repaints per second of a larger area than the few pixels needed to render a caret.


#9

Hi Ed. Did you get any input from Jules about that ?


#10

Yep, thanks for raising this one. TBH I think it’s just very very old code - looking at it now, it doesn’t really need to set its opacity like that, I don’t think it’d make any difference to anyone’s apps if I removed that code just make it non-opaque. People can always just set the opacity themselves if they want to.

(Unless I’m missing a use-case here…? Really the only time there’d be a performance hit would be if you have a really big editor with something behind it that’s very slow to draw, which is probably quite rare)


#11

Great.
note that there are a few other components with similar colour-opacity update (I did not check if they also all expect a “full colour filling” to be done in their respective lookAndFeel methods though) :

MenuWindow

setOpaque (lf.findColour (PopupMenu::backgroundColourId).isOpaque()
                     || ! Desktop::canUseSemiTransparentWindows());

MultiDocumentPanel:

setOpaque (newBackgroundColour.isOpaque());

ListBox::colourChanged()

setOpaque (findColour (backgroundColourId).isOpaque());

TreeView

setOpaque (findColour (backgroundColourId).isOpaque());

ResizableWindow

setOpaque (backgroundColour.isOpaque());