Re: Decreasing seams in scaled windows

Neil <[email protected]> Fri, 22 May 2026 22:23:17 -0700 (PDT)
Newsgroups gmane.comp.lib.scintilla.devel
Message-ID <[email protected]>
------=_Part_121574_68914752.1779513797096
Content-Type: multipart/alternative; 
	boundary="----=_Part_121575_153248915.1779513797096"

------=_Part_121575_153248915.1779513797096
Content-Type: text/plain; charset="UTF-8"
Content-Transfer-Encoding: quoted-printable

Mitchell:
=20

I applied this patch to Qt 6 on macOS and I=E2=80=99m seeing some really we=
ird=20
effects.


Thanks for looking at the change. Some of these can be fixed and others are=
=20
from decisions made when implementing scaling for Win32 and may require=20
application additions or new APIs.
=20

I=E2=80=99m on a Retina display, so that=E2=80=99s probably messing with th=
e=20
devicePixelRatio() computations.


Yes, macOS scales differently which makes it easy for macOS apps but may=20
cause problems when Qt treats scaling more like Windows.

On Windows, scale factors between 100% and 200% are common - I mostly use a=
=20
4K monitor with Windows and Windows recommends 150%.

At 200% (as is common on macOS), it is reasonable to double all dimensions,=
=20
making simple lines 2 pixels wide. At 150%, some people will prefer 1 pixel=
=20
wide lines and some 2. Using floating point widths such as a 1.5 pixel wide=
=20
lines will lead to 'fuzzy' features that are widely disliked. So I left=20
most features with their original pixel widths with the intent of making=20
more choices available so that applications and users can choose something=
=20
that works for them and they can choose when to switch between feature=20
sizes.

See attached noscale.png and yesscale.png images. The arrows in the latter=
=20
point to the weird things I=E2=80=99m seeing:=20
- hard to see indicators


Most indicators do not scale. IndicatorStyle::Diagonal, in particular,=20
tries to fit 3 pixel high underlines in the text descender area which is=20
quite tight so may result in clipping.

With 'noscale.png' macOS Retina scaling, 3 pixels =3D 6 physical pixels whi=
ch=20
looks quite good. 'yesscale.png' is using 3 physical pixels so looks too=20
faint. A new indicator (which I think should have a new name) could be=20
implemented in various ways. The descender height could determine the=20
height and spacing of the diagonal lines although that will decrease=20
density. The stroke width could be made wider although it should probably=
=20
remain an integer - a 1.5 pixel wide line here produces an less distinct=20
blob.
=20

- hard to see margin symbols


The margins are specified in physical pixels so the application should (if=
=20
desired) scale the argument to SCI_SETMARGINWIDTHN. The reason Scintilla=20
doesn't automatically scale these is that symbol margins are particularly=
=20
sensitive to tweaking and I wanted applications to control exactly how wide=
=20
margins are. SciTE provides a '$(scale)' function that may be used until=20
more control is wanted.
=20

- too large autocompletion window


The scale factor was applied twice to the font size and the size and=20
position of the window so there is an update with an inverse scale=20
in ListBoxImpl::SetFont and Window::SetPositionRelative then a compensating=
=20
scale in ListBoxImpl::GetDesiredRect.

This is just part of the division between scaled graphics coordinates used=
=20
by this patch and the need to coordinate with the window layer. Scaling=20
could be handled more thoroughly with the factor flowing through the data=
=20
structures although this would be more intrusive. The implementation here=
=20
is trying to get this working with a small change to the edge of Scintilla,=
=20
just in the Qt platform layer.

A QtScale7.patch that includes these changes is attached.
=20

I looked at the documentation to try and find an option to set scaling, but=
=20
I couldn=E2=80=99t find it. I assume the patch is supposed to detect and ap=
ply=20
scaling via the devicePixelRatio() computations.


Yes, this is trying to be automatic, at least for basic uses. There could=
=20
be an API either to set a scaling factor or just to control whether the=20
scaling is enabled.

Also experimented with Qt 5.15.10 and the only symbol missing=20
was QEvent::DevicePixelRatioChange so there is an #if to avoid that before=
=20
it is available. All the devicePixelRatioF calls appear to work on Qt 5=20
which is great. The DevicePixelRatioChange  event was intercepted as=20
otherwise Haven started up with 200% instead of 150% scaling - it appears=
=20
that the accurate scaling factor is only discovered after the window is=20
shown.

There is a further problem with current Ubuntu+Wayland in that the windows=
=20
for autocompletion and calltips fail as they do not have an owning window=
=20
as their transientParent. Wayland doesn't like applications showing=20
top-level windows at arbitrary locations and wants these to be attached to=
=20
owning windows. This has been a minor problem in the past with GTK but it=
=20
mostly led to windows located poorly rather than failing. The relevant=20
API setTransientParent can't be called after the window creation as=20
creation has already failed and there may need to be some set up during the=
=20
right stage of window construction / realization. This is orthogonal to=20
scaling so may be something that one of the users of Qt on Linux can=20
investigate.

Neil

--=20
You received this message because you are subscribed to the Google Groups "=
scintilla-interest" group.
To unsubscribe from this group and stop receiving emails from it, send an e=
mail to [email protected].
To view this discussion visit https://groups.google.com/d/msgid/scintilla-i=
nterest/eccb497b-61ac-43bd-8d08-3d6422101954n%40googlegroups.com.

------=_Part_121575_153248915.1779513797096
Content-Type: text/html; charset="UTF-8"
Content-Transfer-Encoding: quoted-printable

<div><div dir=3D"auto">Mitchell:<br /></div><div>=C2=A0</div><blockquote st=
yle=3D"margin: 0px 0px 0px 0.8ex; border-left: 1px solid rgb(204, 204, 204)=
; padding-left: 1ex;">I applied this patch to Qt 6 on macOS and I=E2=80=99m=
 seeing some really weird effects.</blockquote><div><br /></div><div>Thanks=
 for looking at the change. Some of these can be fixed and others are from =
decisions made when implementing scaling for Win32 and may require applicat=
ion additions or new APIs.</div><div>=C2=A0</div><blockquote style=3D"margi=
n: 0px 0px 0px 0.8ex; border-left: 1px solid rgb(204, 204, 204); padding-le=
ft: 1ex;"> I=E2=80=99m on a Retina display, so that=E2=80=99s probably mess=
ing with the devicePixelRatio() computations.</blockquote><div><br /></div>=
<div>Yes, macOS scales differently which makes it easy for macOS apps but m=
ay cause problems when Qt treats scaling more like Windows.</div><div><br /=
></div><div>On Windows, scale factors between 100% and 200% are common - I =
mostly use a 4K monitor with Windows and Windows recommends 150%.</div><div=
><br /></div><div>At 200% (as is common on macOS), it is reasonable to doub=
le all dimensions, making simple lines 2 pixels wide. At 150%, some people =
will prefer 1 pixel wide lines and some 2. Using floating point widths such=
 as a 1.5 pixel wide lines will lead to 'fuzzy' features that are widely di=
sliked. So I left most features with their original pixel widths with the i=
ntent of making more choices available so that applications and users can c=
hoose something that works for them and they can choose when to switch betw=
een feature sizes.</div><div><br /></div><blockquote style=3D"margin: 0px 0=
px 0px 0.8ex; border-left: 1px solid rgb(204, 204, 204); padding-left: 1ex;=
">See attached noscale.png and yesscale.png images. The arrows in the latte=
r point to the weird things I=E2=80=99m seeing:
<br />- hard to see indicators</blockquote><div><br /></div><div>Most indic=
ators do not scale. IndicatorStyle::Diagonal, in particular, tries to fit 3=
 pixel high underlines in the text descender area which is quite tight so m=
ay result in clipping.</div><div><br /></div><div>With 'noscale.png' macOS =
Retina scaling, 3 pixels =3D 6 physical pixels which looks quite good. 'yes=
scale.png' is using 3 physical pixels so looks too faint. A new indicator (=
which I think should have a new name) could be implemented in various ways.=
 The descender height could determine the height and spacing of the diagona=
l lines although that will decrease density. The stroke width could be made=
 wider although it should probably remain an integer - a 1.5 pixel wide lin=
e here produces an less distinct blob.</div><div>=C2=A0</div><blockquote st=
yle=3D"margin: 0px 0px 0px 0.8ex; border-left: 1px solid rgb(204, 204, 204)=
; padding-left: 1ex;">- hard to see margin symbols</blockquote><div><br /><=
/div><div>The margins are specified in physical pixels so the application s=
hould (if desired) scale the argument to=C2=A0SCI_SETMARGINWIDTHN. The reas=
on Scintilla doesn't automatically scale these is that symbol margins are p=
articularly sensitive to tweaking and I wanted applications to control exac=
tly how wide margins are. SciTE provides a '$(scale)' function that may be =
used until more control is wanted.</div><div>=C2=A0</div><blockquote style=
=3D"margin: 0px 0px 0px 0.8ex; border-left: 1px solid rgb(204, 204, 204); p=
adding-left: 1ex;">- too large autocompletion window</blockquote><div><br /=
></div><div>The scale factor was applied twice to the font size and the siz=
e and position of the window so there is an update with an inverse scale in=
=C2=A0ListBoxImpl::SetFont and Window::SetPositionRelative then a compensat=
ing scale in ListBoxImpl::GetDesiredRect.</div><div><br /></div><div>This i=
s just part of the division between scaled graphics coordinates used by thi=
s patch and the need to coordinate with the window layer. Scaling could be =
handled more thoroughly with the factor flowing through the data structures=
 although this would be more intrusive. The implementation here is trying t=
o get this working with a small change to the edge of Scintilla, just in th=
e Qt platform layer.</div><div><br /></div><div>A QtScale7.patch that inclu=
des these changes is attached.</div><div>=C2=A0</div><blockquote style=3D"m=
argin: 0px 0px 0px 0.8ex; border-left: 1px solid rgb(204, 204, 204); paddin=
g-left: 1ex;">I looked at the documentation to try and find an option to se=
t scaling, but I couldn=E2=80=99t find it. I assume the patch is supposed t=
o detect and apply scaling via the devicePixelRatio() computations.</blockq=
uote><div><br /></div><div>Yes, this is trying to be automatic, at least fo=
r basic uses. There could be an API either to set a scaling factor or just =
to control whether the scaling is enabled.</div><div><br /></div><div>Also =
experimented with Qt 5.15.10 and the only symbol missing was=C2=A0QEvent::D=
evicePixelRatioChange so there is an #if to avoid that before it is availab=
le. All the=C2=A0devicePixelRatioF calls appear to work on Qt 5 which is gr=
eat. The DevicePixelRatioChange=C2=A0 event was intercepted as otherwise Ha=
ven started up with 200% instead of 150% scaling - it appears that the accu=
rate scaling factor is only discovered after the window is shown.</div><div=
><br /></div><div>There is a further problem with current Ubuntu+Wayland in=
 that the windows for autocompletion and calltips fail as they do not have =
an owning window as their=C2=A0transientParent. Wayland doesn't like applic=
ations showing top-level windows at arbitrary locations and wants these to =
be attached to owning windows. This has been a minor problem in the past wi=
th GTK but it mostly led to windows located poorly rather than failing. The=
 relevant API=C2=A0setTransientParent can't be called after the window crea=
tion as creation has already failed and there may need to be some set up du=
ring the right stage of window construction / realization. This is orthogon=
al to scaling so may be something that one of the users of Qt on Linux can =
investigate.</div><div><br /></div><div>Neil</div></div>

<p></p>

-- <br />
You received this message because you are subscribed to the Google Groups &=
quot;scintilla-interest&quot; group.<br />
To unsubscribe from this group and stop receiving emails from it, send an e=
mail to <a href=3D"mailto:[email protected]">=
[email protected]</a>.<br />
To view this discussion visit <a href=3D"https://groups.google.com/d/msgid/=
scintilla-interest/eccb497b-61ac-43bd-8d08-3d6422101954n%40googlegroups.com=
?utm_medium=3Demail&utm_source=3Dfooter">https://groups.google.com/d/msgid/=
scintilla-interest/eccb497b-61ac-43bd-8d08-3d6422101954n%40googlegroups.com=
</a>.<br />

------=_Part_121575_153248915.1779513797096--

------=_Part_121574_68914752.1779513797096
Content-Type: text/plain; charset=US-ASCII; name=QtScale7.patch
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment; filename=QtScale7.patch
X-Attachment-Id: cdb8f773-00f0-4732-af3a-32ce014bc7cf
Content-ID: <cdb8f773-00f0-4732-af3a-32ce014bc7cf>

diff -r 17e8e54ba9a1 qt/ScintillaEditBase/PlatQt.cpp
--- a/qt/ScintillaEditBase/PlatQt.cpp	Fri May 15 09:12:15 2026 +1000
+++ b/qt/ScintillaEditBase/PlatQt.cpp	Sat May 23 14:06:04 2026 +1000
@@ -228,6 +228,11 @@
 	return std::make_unique<SurfaceImpl>(width, height, mode);
 }
 
+std::unique_ptr<Surface> SurfaceImpl_AllocatePixMap(int width, int height, SurfaceMode mode)
+{
+	return std::make_unique<SurfaceImpl>(width, height, mode);
+}
+
 void SurfaceImpl::SetMode(SurfaceMode mode_)
 {
 	mode = mode_;
@@ -308,7 +313,7 @@
 
 int SurfaceImpl::DeviceHeightFont(int points)
 {
-	return points;
+	return points * device->devicePixelRatioF();
 }
 
 void SurfaceImpl::LineDraw(Point start, Point end, Stroke stroke)
@@ -795,6 +800,9 @@
 		painter->setRenderHint(QPainter::TextAntialiasing, true);
 
 		painter->setRenderHint(QPainter::Antialiasing, true);
+
+		const qreal scale = device->devicePixelRatioF();
+		painter->scale(1.0 / scale, 1.0 / scale);
 	}
 
 	return painter;
@@ -853,6 +861,8 @@
 
 void Window::SetPositionRelative(PRectangle rc, const Window *relativeTo)
 {
+	const qreal scale = wid ? window(wid)->devicePixelRatioF() : 1.0;
+	rc = rc / scale;
 	QPoint oPos = window(relativeTo->wid)->mapToGlobal(QPoint(0,0));
 	int ox = oPos.x();
 	int oy = oPos.y();
@@ -883,7 +893,9 @@
 PRectangle Window::GetClientPosition() const
 {
 	// The client position is the window position
-	return GetPosition();
+	const qreal scale = wid ? window(wid)->devicePixelRatioF() : 1.0;
+	const PRectangle rc = GetPosition();
+	return rc * scale;
 }
 
 void Window::Show(bool show)
@@ -900,8 +912,15 @@
 
 void Window::InvalidateRectangle(PRectangle rc)
 {
-	if (wid)
-		window(wid)->update(QRectFromPRect(rc));
+	if (wid) {
+		//const qreal scale = window(wid)->devicePixelRatioF();
+		// Avoids aliasing leaving a partial bottom pixel
+		//rc = rc / scale;
+		//rc.top -= 20;
+		//rc.bottom += 2;
+		//window(wid)->update(QRectFromPRect(rc));
+		window(wid)->update();
+	}
 }
 
 void Window::SetCursor(Cursor curs)
@@ -1051,7 +1070,11 @@
 	ListWidget *list = GetWidget();
 	const FontAndCharacterSet *pfacs = AsFontAndCharacterSet(font);
 	if (pfacs && pfacs->pfont) {
-		list->setFont(*(pfacs->pfont));
+		QFont fontDeScaled = *(pfacs->pfont);
+		const qreal scale = window(wid)->devicePixelRatioF();
+		qreal pointSize = fontDeScaled.pointSizeF() / scale;
+		fontDeScaled.setPointSizeF(pointSize);
+		list->setFont(fontDeScaled);
 	}
 }
 void ListBoxImpl::SetAverageCharWidth(int /*width*/) {}
@@ -1081,7 +1104,8 @@
 		width += style->pixelMetric(QStyle::PM_ScrollBarExtent);
 	}
 
-	return PRectangle(0, 0, width, height);
+	const qreal scale = window(wid)->devicePixelRatioF();
+	return PRectangle(0, 0, width, height) * scale;
 }
 int ListBoxImpl::CaretFromEdge()
 {
diff -r 17e8e54ba9a1 qt/ScintillaEditBase/PlatQt.h
--- a/qt/ScintillaEditBase/PlatQt.h	Fri May 15 09:12:15 2026 +1000
+++ b/qt/ScintillaEditBase/PlatQt.h	Sat May 23 14:06:04 2026 +1000
@@ -164,6 +164,8 @@
 	QPainter *GetPainter();
 };
 
+std::unique_ptr<Surface> SurfaceImpl_AllocatePixMap(int width, int height, SurfaceMode mode);
+
 }
 
 #endif
diff -r 17e8e54ba9a1 qt/ScintillaEditBase/ScintillaEditBase.cpp
--- a/qt/ScintillaEditBase/ScintillaEditBase.cpp	Fri May 15 09:12:15 2026 +1000
+++ b/qt/ScintillaEditBase/ScintillaEditBase.cpp	Sat May 23 14:06:04 2026 +1000
@@ -127,6 +127,12 @@
 	} else if (event->type() == QEvent::Hide) {
 		setMouseTracking(false);
 		result = QAbstractScrollArea::event(event);
+#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0)
+    } else if (event->type() == QEvent::DevicePixelRatioChange) {
+		// Reset cached data
+		sqt->InvalidateStyleRedraw();
+        result = QAbstractScrollArea::event(event);
+#endif
 	} else {
 		result = QAbstractScrollArea::event(event);
 	}
@@ -318,7 +324,7 @@
 
 void ScintillaEditBase::mousePressEvent(QMouseEvent *event)
 {
-	const Point pos = PointFromQPoint(event->pos());
+	const Point pos = PointFromQPoint(event->pos()) * devicePixelRatioF();
 
 	emit buttonPressed(event);
 
@@ -347,7 +353,8 @@
 
 void ScintillaEditBase::mouseReleaseEvent(QMouseEvent *event)
 {
-	const QPoint point = event->pos();
+	const QPoint point = event->pos() * devicePixelRatioF();
+
 	if (event->button() == Qt::LeftButton)
 		sqt->ButtonUpWithModifiers(PointFromQPoint(point), TimeOfEvent(time), ModifiersOfKeyboard());
 
@@ -367,7 +374,7 @@
 
 void ScintillaEditBase::mouseMoveEvent(QMouseEvent *event)
 {
-	const Point pos = PointFromQPoint(event->pos());
+	const Point pos = PointFromQPoint(event->pos()) * devicePixelRatioF();
 
 	const bool shift = QApplication::keyboardModifiers() & Qt::ShiftModifier;
 	const bool ctrl  = QApplication::keyboardModifiers() & Qt::ControlModifier;
diff -r 17e8e54ba9a1 qt/ScintillaEditBase/ScintillaQt.cpp
--- a/qt/ScintillaEditBase/ScintillaQt.cpp	Fri May 15 09:12:15 2026 +1000
+++ b/qt/ScintillaEditBase/ScintillaQt.cpp	Sat May 23 14:06:04 2026 +1000
@@ -801,13 +801,22 @@
 
 void ScintillaQt::PartialPaint(const PRectangle &rect)
 {
-	rcPaint = rect;
+	QWidget *widget = static_cast<QWidget *>(wMain.GetID());
+	const qreal scale = widget->devicePixelRatioF();
+
+	rcPaint = rect * scale;
 	paintState = PaintState::painting;
 	PRectangle rcClient = GetClientRectangle();
 	paintingAllText = rcPaint.Contains(rcClient);
 
+	// Create Bitmap of exactly window size then blit after Paint call
+	const int w = static_cast<int>(rcClient.Width() * scale);
+	const int h = static_cast<int>(rcClient.Height() * scale);
+	std::unique_ptr<Surface> surfPix = SurfaceImpl_AllocatePixMap(w, h, CurrentSurfaceMode());
+	PRectangle rcScaled = PRectangle::FromInts(0,0,w,h);
+	Paint(surfPix.get(), rcScaled);
 	AutoSurface surfacePaint(this);
-	Paint(surfacePaint, rcPaint);
+	surfacePaint->Copy(rcScaled, Point(0,0), *surfPix);
 	surfacePaint->Release();
 
 	if (paintState == PaintState::abandoned) {
diff -r 17e8e54ba9a1 src/Geometry.h
--- a/src/Geometry.h	Fri May 15 09:12:15 2026 +1000
+++ b/src/Geometry.h	Sat May 23 14:06:04 2026 +1000
@@ -45,6 +45,10 @@
 		return Point(x - other.x, y - other.y);
 	}
 
+	constexpr Point operator*(XYPOSITION multiplier) const noexcept {
+		return Point(x * multiplier, y * multiplier);
+	}
+
 	// Other automatically defined methods (assignment, copy constructor, destructor) are fine
 };
 
@@ -101,6 +105,15 @@
 		return (rc.left == left) && (rc.right == right) &&
 			(rc.top == top) && (rc.bottom == bottom);
 	}
+	
+	constexpr PRectangle operator*(XYPOSITION multiplier) const noexcept {
+		return PRectangle(left * multiplier, top * multiplier, right * multiplier, bottom * multiplier);
+	}
+
+	constexpr PRectangle operator/(XYPOSITION divisor) const noexcept {
+		return PRectangle(left / divisor, top / divisor, right / divisor, bottom / divisor);
+	}
+
 	constexpr bool Contains(Point pt) const noexcept {
 		return (pt.x >= left) && (pt.x <= right) &&
 			(pt.y >= top) && (pt.y <= bottom);

------=_Part_121574_68914752.1779513797096--