Int vs float rectangles with scaling: a proposal for Windows/MacOS consistency for JUCE 8

Sure, and this particular issue impacts both MacOS and Windows (albeit to different degrees, more on that later), but that is not exactly the problem I am talking about here.
My main issue is with the way int and float rectangles behave differently when scaled on Windows.

Here is an example illustrating these int vs float differences:

class TestComponent : public juce::Component {
public:
	TestComponent() {
		for (auto& c : mFillAlls) addAndMakeVisible(c);
		for (auto& c : mFillRectInts) addAndMakeVisible(c);
		for (auto& c : mFillRectFloats) addAndMakeVisible(c);
	}

	void resized() override {
		auto bounds = getLocalBounds();

		mTextColumn = bounds.removeFromLeft(250);

		auto fillAllComponentRow = bounds.removeFromTop(60);
		auto fillRectIntComponentRow = bounds.removeFromTop(60);
		auto fillRectFloatComponentRow = bounds.removeFromTop(60);

		for (auto& c : mFillAlls) c.setBounds(fillAllComponentRow.removeFromLeft(60).reduced(1));
		for (auto& c : mFillRectInts) c.setBounds(fillRectIntComponentRow.removeFromLeft(60).reduced(1));
		for (auto& c : mFillRectFloats) c.setBounds(fillRectFloatComponentRow.removeFromLeft(60).reduced(1));

		mIntRectRow = bounds.removeFromTop(60);
		mFloatRectRow = bounds.removeFromTop(60);

		mIntRectJunctionsRow = bounds.removeFromTop(60);
		mFloatRectJunctionsRow = bounds.removeFromTop(60);
		mMixedRectJunctionsRow = bounds.removeFromTop(60);
	}

	void paint(juce::Graphics& g) override {
		g.fillAll(juce::Colours::white);

		auto textColumn = mTextColumn;

		for (auto& text : mTexts) {
			g.drawText(text, textColumn.removeFromTop(60).reduced(10), juce::Justification::centredLeft);
		}

		auto intRectRow = mIntRectRow;
		auto floatRectRow = mFloatRectRow;
		auto intRectJunctionsRow = mIntRectJunctionsRow;
		auto floatRectJunctionsRow = mFloatRectJunctionsRow;
		auto mixedRectJunctionsRow = mMixedRectJunctionsRow;

		g.setColour(juce::Colours::black.withAlpha(0.5f));
		for (int i = 0; i < 4; ++i) {
			g.fillRect(intRectRow.removeFromLeft(60).reduced(1));
			g.fillRect(floatRectRow.removeFromLeft(60).reduced(1).toFloat());

			g.fillRect(intRectJunctionsRow.removeFromLeft(60).reduced(0, 10));
			g.fillRect(floatRectJunctionsRow.removeFromLeft(60).reduced(0, 10).toFloat());

			if (i & 1) {
				g.fillRect(mixedRectJunctionsRow.removeFromLeft(60).reduced(0, 10).toFloat());
			}
			else {
				g.fillRect(mixedRectJunctionsRow.removeFromLeft(60).reduced(0, 10));
			}
		}
	}

private:
	struct FillAll : public juce::Component {
		void paint(juce::Graphics& g) override {
			g.fillAll();
		}
	};

	struct FillRectInt : public juce::Component {
		void paint(juce::Graphics& g) override {
			g.fillRect(getLocalBounds());
		}
	};

	struct FillRectFloat : public juce::Component {
		void paint(juce::Graphics& g) override {
			g.fillRect(getLocalBounds().toFloat());
		}
	};

	juce::Rectangle<int> mTextColumn, mIntRectRow, mFloatRectRow, mIntRectJunctionsRow, mFloatRectJunctionsRow, mMixedRectJunctionsRow;
	std::array<FillAll, 4> mFillAlls;
	std::array<FillRectInt, 4> mFillRectInts;
	std::array<FillRectFloat, 4> mFillRectFloats;

	juce::StringArray mTexts{
		"fillAll components",
		"fillRect int components",
		"fillRect float components",
		"fillRect int paint",
		"fillRect float paint",
		"fillRect int paint junctions",
		"fillRect float paint junctions",
		"fillRect alterning paint junctions"
	};

	JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(TestComponent)
};

class MainComponent  : public juce::Component {
public:
	MainComponent() {
		setSize(referenceWidth, referenceWidth);
		addAndMakeVisible(component);
		addAndMakeVisible(zoomLabel);
		refreshZoomLabel();
	}

	void resized() override {
		refreshZoomLabel();
		zoomLabel.setBounds(getLocalBounds().removeFromTop(30));
		component.setBounds(0, 0, referenceWidth, referenceWidth);
		component.setTransform(juce::AffineTransform::scale(getScale()).followedBy(juce::AffineTransform::translation(0,30)));
	}

private:
	void refreshZoomLabel() {
		zoomLabel.setText("zoom: " + juce::String(juce::roundToInt(100 * getScale())) + "%", juce::dontSendNotification);
	}

	float getScale() {
		return (float)getWidth() / referenceWidth;
	}

	juce::Label zoomLabel;
	TestComponent component;

	int referenceWidth = 600;

	JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainComponent)
};

You can compile it and play with the scale by resizing the window, and see how rectangles behave.
On Windows Int rectangles (and components) are bouncing around, and their 2px margins start to disappear as soon as you start scaling down the window (as soon as 98% with a 100% display scale). By 65% the margins are invisible most of the time.

I also added three rows at the end with adjacent rectangles to illustrate what @chkn mentioned.
In addition to seams (and even overlaps on windows, which are visible becauses the fill colour has a 0.5f alpha), the last row illustrates the misalignments you get when mixing int and float rectangles.

Once gain, these int rectangle geometry problems also impact components filled with fillAll or fillRect, as illustrated by the first 3 rows.

All these problems are not present on MacOS, where int and float rectangle (and components bounds) behave exactly the same (and are well behaved), and can be mixed without alignement problems.

Here are a few screenshots of a selection of scales on both Windows and MacOS, both using the default renderer as of JUCE 7.0.11:

Windows, 100% Display scale, Juce scales: 100%, 103%, 94%, 54%

IntVsFloat_Windows_54_percent

MacOS, 100% Display scale, Juce scales: 100%, 103%, 94%, 54% :

IntVsFloat_MacOS_54_percent

9 Likes