Cursor scaling issues on Windows HiDPI

Hi all,

we’re seeing an issue on Windows 10 with cursors being scaled incorrectly on HiDPI screens. The custom cursor image appears tiny, although the rest of the UI is being scaled correctly.

Additionally, contrary to image components or e.g. image buttons, there is no provision to use higher-resolution cursor images (pushing a 2x size image into a 1x size rect, I mean), to account for scaling at the moment, so if scaling were to work, the image would be bitmap-stretched, correct? [edit] Doublechecking on macOS shows that they appear to be bitmap-stretched indeed.

Best,
Andreas

I found this topic from last year in the general discussion:

Apparently I’m not the only one with the problem, but it also doesn’t seem to have created a lot of traction so far…

We rolled our own solution, which means that we store the cursors at double the resolution a 100% display would need and then scale down/up from there. We use the scale of the desktop the window is on to determine the scale factor as the default cursors (arrow etc.) are also scaled that way by the OS.

@reFX I see, so you use JUCE::MouseCursor to create a (new) cursor with the “correct” resolution depending on the display you’re on? Or you mean you don’t use juce::MouseCursor at all?

Sorry, I remembered it slightly wrong. So we have SVG in memory that we convert to an Image when the scale or cursor changes. We use the normal juce::MouseCursor and we use the constructor that takes an image. We get the scale from the associated components x,y coordinates and then get the desktop and its scale from that.

Here is our GUI_Cursors.h file, but you will have to code the

reFX::SVG::loadSVG

function yourself.

In our case, it loads the SVG from the BinaryData into a juce::Drawable.

#pragma once

//-------------------------------------------------------------------------------------------------
class GUI_Cursors
{
public:
	GUI_Cursors ()  = default;

	MouseCursor	getPencil ( Component& c )
	{
		return getCursor ( pencil, pencilScale, "cursor-pencil-alt.svg", c, 24, 24 );
	}

	MouseCursor	getPaint ( Component& c )
	{
		return getCursor ( paint, paintScale, "cursor-brush.svg", c, 24, 24 );
	}

	MouseCursor	getLasso ( Component& c )
	{
		return getCursor ( lasso, lassoScale, "cursor-select.svg", c, 28, 54 );
	}

	MouseCursor	getEraser ( Component& c )
	{
		return getCursor ( eraser, eraserScale, "cursor-delete.svg", c, 30, 51, true );
	}

	MouseCursor	getHandRock ( Component& c )
	{
		return getCursor ( handRock, handRockScale, "cursor-hand-rock.svg", c, 24, 24 );
	}

	MouseCursor	getHandPaper ( Component& c )
	{
		return getCursor ( handPaper, handPaperScale, "cursor-hand-paper.svg", c, 24, 24 );
	}

private:
	MouseCursor getCursor ( MouseCursor& mc, float& lastScale, const String& svg, Component& c, const int cx, const int cy, const bool danger = false )
	{
		auto scale = getScale ( c );
		if ( lastScale != scale )
		{
			lastScale = scale;
			mc = cursorFromSvg ( svg, scale, cx, cy, danger );
		}

		return mc;
	}

	float getScale ( [[ maybe_unused ]] Component& c )
	{
		#if JUCE_MAC
			return 1.0f;
		#else
			auto rc = c.localAreaToGlobal ( c.getLocalBounds () );
			return float ( Desktop::getInstance ().getDisplays ().findDisplayForRect ( rc ).scale );
		#endif
	}

	MouseCursor cursorFromSvg ( const String& svg, const float scale, const int cx, const int cy, const bool danger )
	{
		const auto	ix = float ( cx ) * scale;
		const auto	iy = float ( cy ) * scale;
		const auto	id = int ( std::max ( ix, iy ) );

		// Cursor images have to be square (on Windows at least), otherwise they get stretched
		Image img ( Image::ARGB, id, id, true );

		{
			Graphics g ( img );

			auto	drawable = reFX::SVG::loadSVG ( svg );

			#if JUCE_MAC
				drawable->replaceColour ( Colour ( 0xffff00ff ), danger ? Colour ( 0xffffaa44 ) : Colours::black );
				drawable->replaceColour ( Colour ( 0xff00ff00 ), danger ? Colour ( 0xff443311 ) : Colours::white );
			#else
				drawable->replaceColour ( Colour ( 0xffff00ff ), danger ? Colour ( 0xffffaa44 ) : Colours::white );
				drawable->replaceColour ( Colour ( 0xff00ff00 ), danger ? Colour ( 0xff443311 ) : Colours::black );
			#endif

			drawable->drawWithin ( g, Rectangle<float> { ix, iy }, 0, 1.0f );
		}

		const auto	inset = int ( 4.0f * scale );
		return MouseCursor ( img, inset, inset );
	}

	MouseCursor	pencil;
	MouseCursor	paint;
	MouseCursor	lasso;
	MouseCursor	eraser;
	MouseCursor	handRock;
	MouseCursor	handPaper;

	float	pencilScale = 0.0f;
	float 	paintScale = 0.0f;
	float	lassoScale = 0.0f;
	float	eraserScale = 0.0f;
	float	handRockScale = 0.0f;
	float	handPaperScale = 0.0f;
};
3 Likes

Thanks, that makes sense of course. I had the same basic idea but didn’t get around to implementing it yet. Will have a look in the near future.

@reuk Any plans on implementing something like that int JUCE directly? In my opinion, the current behaviour on Win HiDPI without this workaround is a bug.

@reFX @reuk I’m finally working on this, but I ran into a problem with the scale factor/dpi information. As I’m acting on top of an OpenGL context here, I’m using

Point<int> p = juce::Desktop::getInstance().getMousePosition();
		juce::Displays::Display display = juce::Desktop::getInstance().getDisplays().findDisplayForPoint(p);

To get the display where I want to display my cursor, but unfortunately, I don’t seem to get the correct scale or dpi value, although findDisplayForPoint() correctly returns either my macbook retina screen or my fhd external screen (correct resolution, isMain true or false…looks correct). In both cases the scale factor is 1 (actually I set a global scale factor of .85, in which case it’s always .85) and dpi is 192.

Strangely, the plugin itself is scaled correctly on both screens, implying that the correct scale and dpi info IS available, however I can’t access it via Display::scale and Display::dpi.

Am I missing something obvious here?

Best,
Andreas

In a plugin, the scale information is provided by the host. The recommended way to access this scale information is to call Component::getApproximateScaleFactorForComponent passing the plugin’s editor component. If you want to display a custom cursor for a particular component, you should probably use the scale factor of that component to set the cursor scale.

Thank you very much. I still wonder why the dpi info is not correct when accessing Display::dpi, though? I’m using Reaper 6.27 with the per monitor dpi aware setting enabled to test this, btw.

On Windows, only standalone JUCE apps are DPI aware. Plugins are not, by design. Instead, the host has sole control of the scale factor of each plugin editor.

I see, but sorry if I’m sounding stupid here, you mean the DPI field of the Display object is being filled (incorrectly) by the host? Because the host obviously scales the plugin correctly when moving from one display to another, yet the DPI info inside the Display object my editor/component is currently drawn on is wrong.

No, the plugin chooses not to populate this information, because the plugin should not attempt to make its own decisions about what scale factor to use.

1 Like

Hi reuk – I’m facing a similar issue (not to co-opt this thread) where if I am using Displays::getDisplayForPoint, most popular Windows DAWs do not return the scale factor other than “1”, so we get into a situation where our plugin is enormous. Only Ableton and Reaper with Multimonitor Aware v2 (experimental) settings seem to handle this correctly.

Based on your comment is there no recourse to try to avoid getting incorrect data? I see that the DPI value is correct but scale remains at 1.

It sounds like you’re trying to set your editor’s scale based on the DPI of the display. This is not the job of the plugin to decide, so you should probably avoid doing this. Instead, you should leave it to the host to control the scale factor. This should work by default in JUCE plugins, and there shouldn’t be any need to manually compensate for the resolution of the display.

In hosts which are DPI-aware, you should see the host calling the setScaleFactor function on your AudioProcessorEditor when the plugin view is moved between displays with different resolutions. In Reaper, when I open the DSPModulePluginDemo on a hi-DPI display, I see that setScaleFactor is called with a value of 1.75, and the editor renders at the appropriate size. If I open the editor on a lower-resolution display, setScaleFactor is not called and the editor displays with no additional scaling.

Hi reuk,

Thanks for the insight. We do have a user-controllable scale feature on our plugin UI which is where we are going wrong right now.