[FIXED]Component::isMouseOver() not considering Transform scaling?

Hi there,
I’ve recently stumbled about this issue when applying an AffineTransform::Scale on a Viewport, but I think this is a more generic issue.
Basically, when I scale my viewport using, let’s say, a factor of 1.5, the thumb of the scrollbars don’t always seem to be repainted correctly when the mouse cursor is on them.
I think this is because the paint routine implemented by ScrollBar makes use of Component::isMouseOver(), which to me looks like it’s not checking for transformations that are currently being applied to the component.
Any input on this?
Cheers!

I think this is actually due to a rounding error in Component::isMouseOver(). I’ve just pushed a fix to develop here - can you test it and see if it works for you?

1 Like

Yep, that’s it!
Thanks :slight_smile:

1 Like

Thanks, this actually helped me a bunch, with correctly determining if the mouse cursor is actually over a component inside a transformed component.
That fix still hasn’t made it to the master branch as of today.

Bumping this again! We ran into this issue since we’re using AffineTransform. Applying @ed95’s one line fix to our local JUCE fixed it. This isn’t on JUCE master as of 6.0.8.

EDIT: Oddly, this fix improved the problem on Debug builds, but our Release builds are still acting weird. I’m wondering if it’s optimizing out something?

Oh my, I’ve just realised too that the fix didn’t make it into the master branch…
Bumpety bump?

(Re-)Added in:

1 Like

Thanks ed!

Thanks, @ed95!

I think there might still be a rounding error somewhere. This has improved our rollover bug, but only (oddly) in one x direction. Here’s an example .gif:

lionMouse

As you can see, when moving the cursor from right-to-left, isMouseOver is triggering properly on our modulation pin widgets. However, moving from left-to-right misses almost all of them.

At 1.0x plugin scaling, this behavior doesn’t occur.

Can you post a simple example which reproduces the problem? That would help to track this down.

It’d be helpful to see where you are calling isMouseOver() from. I’ve been unable to reproduce the issue with this example:

class MainComponent  : public juce::Component
{
public:
    //==============================================================================
    MainComponent()
    {
        addAndMakeVisible (contentComponent);
        contentComponent.setTransform (juce::AffineTransform::scale (5.0f));
        
        setSize (300, 200);
    }

    ~MainComponent() override
    {
    }

    //==============================================================================
    void paint (juce::Graphics& g) override
    {
        // (Our component is opaque, so we must completely fill the background with a solid colour)
        g.fillAll (getLookAndFeel().findColour (juce::ResizableWindow::backgroundColourId));
    }

    void resized() override
    {
        contentComponent.setBounds (contentComponent.getLocalArea (this, getLocalBounds()));
    }

private:
    //==============================================================================
    struct ContentComponent  : public juce::Component
    {
        ContentComponent()
        {
            for (auto& w : widgets)
                addAndMakeVisible (w);
        }

        ~ContentComponent() override
        {
        }

        //==============================================================================
        void paint (juce::Graphics& g) override
        {
            // (Our component is opaque, so we must completely fill the background with a solid colour)
            g.fillAll (getLookAndFeel().findColour (juce::ResizableWindow::backgroundColourId).darker());
        }

        void resized() override
        {
            auto bottomSlice = getLocalBounds().removeFromBottom (getHeight() / 5);
            const auto width = bottomSlice.getWidth() / (int) widgets.size();
            
            for (auto& w : widgets)
                w.setBounds (bottomSlice.removeFromLeft (width).reduced (1));
        }

        struct MouseOverWidget  : public juce::Component
        {
            void mouseEnter (const juce::MouseEvent&) override
            {
                jassert (isMouseOver());
                
                highlighted = true;
                repaint();
            }
            
            void mouseExit (const juce::MouseEvent&) override
            {
                jassert (! isMouseOver());
                
                highlighted = false;
                repaint();
            }
            
            void paint (juce::Graphics& g) override
            {
                g.fillAll (highlighted ? juce::Colours::hotpink : juce::Colours::cyan);
            }
            
            bool highlighted = false;
        };
        
        std::array<MouseOverWidget, 10> widgets;
    };
    
    ContentComponent contentComponent;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainComponent)
};

Thanks @ed95, I’ll put together a minimal example today.

In general, we check isMouseOver() inside of paint()

In the PinComponent constructor, we call setRepaintsOnMouseActivity(true);

The other primary difference is our scaling code. We have an empty plugin window that is an AudioProcessorEditor and it loads a Component (editorComponent) that is the actual plugin interface. That interface is scaled using AffineTransform in the window’s resized() callback:

editorComponent->setTransform(AffineTransform::scale(newScale));

The behavior in the GIF is occurring on Windows at any desktop scaling value. We’re not using OpenGL on our interfaces anymore.

Hi @ed95, I was able to successfully reproduce the bug in a minimal project.

When the project opens it is at 1x scaling and the lights respond to mouseOver correctly. However, you can change the scale of the editor component by resizing the window vertically. At most scales, the mouseOver breaks when moving from left-to-right, but not right-to-left.

ScalingTest.zip (5.9 KB)

I know that recent juce has done some weird stuff with repaints on mac. I wonder if the left to right vs. right to left could actually be some bad logic in the need-to-repaint check. If we run it with the debug paint enabled we’d be able to see if it’s not repainting when going left to right.

With further testing by @amusesmile, we’ve figured out the following:

  1. This bug also occurs on Mac with the minimal example project above.
  2. Without the rounding error patch above, the behavior is inconsistent when moving the mouse in either direction.
  3. With the rounding error patch above, left-to-right rarely works, but right-to-left almost always works (like in my gif).

I’ve figured out more of what is happening and understand what’s going on. As predicted going from top to bottom is worse than going from bottom to top. There are kind of two things that are causing the issue for us- first, the JUCE team recently seems to have added some optimization with repainting so it only repaints once when you enter/exit a component instead of constantly as the mouse moves within it. I get what they’re trying to do but it’s darn annoying and causes all sorts of bugs like what we’re seeing here. (One quick solution would be to turn this off or back to how it used to be but that’s a side discussion.)

Anyway, in old JUCE, when you were moving your mouse over a component it was constantly repainting, so if the first check was localCoords x: 0 y:12 for instance, it wouldn’t pass the reallyContain() check but the next millisecond you’d be at local point x: 2 y: 12 let’s say, and that would pass and draw correctly. In our situation the “real” bug is that the second you enter the component from the left or top, there’s a non-zero chance that the x coordinate returned by c->getLocalPoint with the mouse coordinates will be x: 0, or the y coordinate will be y: 0. This doesn’t pass the reallyContains() check so it is repainted once thinking that the mouse isn’t within it, and never again until the mouse exits. No good.

Tracing it further through the isMouseOver check, we go through this cascade: Component::reallyContains goes to Component::contains which goes to ComponentHelpers::hitTest which checks bool isPositiveAndBelow. Zero actually passes this, which is great, so the first “contains()” check in Component::reallyContains passes and everything would be great EXCEPT this function goes one more step and checks what component the top level thinks is at that coordinate to check the match. For some reason getComponentAt won’t return the individual button if one of those local coordinates is zero. Instead it returns the background component and the check fails.

The relevant thing that’s not passing ends up being here, basically, in Component::reallyContains:

auto* compAtPosition = top->getComponentAt (top->getLocalPoint (this, point));

This returns the whole background instead of the individual button component, if the local coordinates are something like x: 0, y: 12

I haven’t traced it down inside of “top->getComponentAt” to figure out why a local zero coordinate doesn’t pass but that would be the next stage.

Alternatively, if anyone knows how to change it back to constantly repainting when the mouse is moved over a component, do please let me know. Thanks!

2 Likes

Thanks for the example. The following patch improves things considerably for me, can you try applying it and see if it works for you too? If it does, I’ll look at getting it merged into develop.

0001-Improved-reliability-of-Component-reallyContains-whe.patch (1.7 KB)

Wow thanks! In testing, this seems to not solve the problem as written because the change in “getComponentAt” makes it so that we can get a -1 x/y coordinate inside of isMouseOver, however if I only keep the change in “reallyContains” it seems to work. I need to stash all of my changes and do some more testing to make sure that’s the right answer but it seems we’re on the right track. Cheers.

Thanks! It’s somewhat better, but I’m still seeing a lot of missing mouseovers when moving from left to right. I’ve tried what @amusesmile said and applied the first patch line first, and then both patch lines. I think the first line only is marginally better, but not by much.

Ok, I’ve had another look into this and it should now be resolved with the following commit:

Let me know if you’re still experiencing issues after this change.

1 Like