[PATCH] Autocompletion list image scaling for HiDPI/Retina displays

Mitchell <[email protected]> Sat, 10 May 2025 11:46:33 -0400
Newsgroups gmane.comp.lib.scintilla.devel
Message-ID <[email protected]>
Hi Neil,

Attached is a patch and zip against Scintilla 5.5.6 that implements SCI_AUTOCSETIMAGESCALE/SCI_AUTOCGETIMAGESCALE for Qt and GTK 3.10+ along with documentation, based on our previous thread[1].

The only questionable bit in my opinion is the GTK implementation switches the list store image column type to CAIRO_GOBJECT_TYPE_SURFACE from GDK_TYPE_PIXBUF. Not only does this require the <cairo/cairo-gobject.h> header (which is included with my default libgtk-3-dev installation), but it also performs a copy for each pixbuf to be included in the list. This copy is necessary because Cairo surfaces store device scale, not GDK Pixbufs[2]. The list store takes ownership of the surface (it’s a boxed pointer), and will free it when done[3]. I was not able to figure out how to create a Cairo surface from an incoming pixbuf, g_object_ref() it, and pass it to the list store directly (thus avoiding copies and a premature deallocation).

For the Qt version, you’ll notice I’m making `im.width() / im.devicePixelRatio()` calls instead of `im.deviceIndependentSize().width()`. That is because the latter was introduced in Qt 6.2, but Scintilla still supports Qt 5.

Thanks for your consideration!

Cheers,
Mitchell

[1]: https://groups.google.com/g/scintilla-interest/c/hxSnbpnjED4
[2]: https://gitlab.gnome.org/GNOME/gtk/-/issues/613
[3]: https://gitlab.gnome.org/GNOME/gtk/-/issues/613#note_687918

-- 
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 email to [email protected].
To view this discussion visit https://groups.google.com/d/msgid/scintilla-interest/3B0945C1-2DA5-4A76-AD23-39E6E3584C06%40foicica.com.
auto-c-image-scale.patch (application/octet-stream, 13.5 KB)
From 095aac3d84086921318e2c0bc29bb7b053b079bf Mon Sep 17 00:00:00 2001
From: orbitalquark <[email protected]>
Date: Sat, 10 May 2025 11:17:54 -0400
Subject: [PATCH] Add SCI_AUTOC{G,S}ETIMAGESCALE and implement for Qt and GTK.

---
 call/ScintillaCall.cxx          |  8 ++++++++
 doc/ScintillaDoc.html           | 12 ++++++++++++
 gtk/PlatGTK.cxx                 | 34 +++++++++++++++++++++++++++++----
 include/Scintilla.h             |  2 ++
 include/Scintilla.iface         |  6 ++++++
 include/ScintillaCall.h         |  2 ++
 include/ScintillaMessages.h     |  2 ++
 qt/ScintillaEditBase/PlatQt.cpp | 24 +++++++++++++----------
 src/AutoComplete.cxx            |  1 +
 src/AutoComplete.h              |  1 +
 src/Platform.h                  |  1 +
 src/ScintillaBase.cxx           |  7 +++++++
 test/simpleTests.py             |  5 +++++
 13 files changed, 91 insertions(+), 14 deletions(-)

diff --git a/call/ScintillaCall.cxx b/call/ScintillaCall.cxx
index 662bb940..11a1f1d5 100644
--- a/call/ScintillaCall.cxx
+++ b/call/ScintillaCall.cxx
@@ -1115,6 +1115,14 @@ int ScintillaCall::AutoCGetStyle() {
 	return static_cast<int>(Call(Message::AutoCGetStyle));
 }
 
+void ScintillaCall::AutoCSetImageScale(int scalePercent) {
+	Call(Message::AutoCSetImageScale, scalePercent);
+}
+
+int ScintillaCall::AutoCGetImageScale() {
+	return static_cast<int>(Call(Message::AutoCGetImageScale));
+}
+
 void ScintillaCall::SetIndent(int indentSize) {
 	Call(Message::SetIndent, indentSize);
 }
diff --git a/doc/ScintillaDoc.html b/doc/ScintillaDoc.html
index 28010507..c591c86d 100644
--- a/doc/ScintillaDoc.html
+++ b/doc/ScintillaDoc.html
@@ -6475,6 +6475,8 @@ struct Sci_TextToFindFull {
      <a class="message" href="#SCI_AUTOCGETMAXWIDTH">SCI_AUTOCGETMAXWIDTH &rarr; int</a><br />
      <a class="message" href="#SCI_AUTOCSETSTYLE">SCI_AUTOCSETSTYLE(int style)</a><br />
      <a class="message" href="#SCI_AUTOCGETSTYLE">SCI_AUTOCGETSTYLE &rarr; int</a><br />
+     <a class="message" href="#SCI_AUTOCSETIMAGESCALE">SCI_AUTOCSETIMAGESCALE(int scale)</a><br />
+     <a class="message" href="#SCI_AUTOCGETIMAGESCALE">SCI_AUTOCGETIMAGESCALE &rarr; int</a><br />
      <a class="element" href="#SC_ELEMENT_LIST">SC_ELEMENT_LIST : colouralpha</a><br />
      <a class="element" href="#SC_ELEMENT_LIST_BACK">SC_ELEMENT_LIST_BACK : colouralpha</a><br />
      <a class="element" href="#SC_ELEMENT_LIST_SELECTED">SC_ELEMENT_LIST_SELECTED : colouralpha</a><br />
@@ -6706,6 +6708,16 @@ struct Sci_TextToFindFull {
       result as the argument to <code>SCI_AUTOCSETSTYLE</code> and <code>SCI_STYLESETFONT</code> and others.
     </p>
 
+    <p>
+      <b id="SCI_AUTOCSETIMAGESCALE">SCI_AUTOCSETIMAGESCALE(int scalePercent)</b><br />
+      <b id="SCI_AUTOCGETIMAGESCALE">SCI_AUTOCGETIMAGESCALE &rarr; int</b><br />
+
+      Get or set the scale factor in percent for <em>all</em> autocompletion list images. This is useful on macOS with
+      a retina display where each display unit is 2 pixels: use a factor of 200 so that each image pixel is displayed using a screen pixel.
+      The default scale, 100, will stretch each image pixel to cover 4 screen pixels on a retina display.
+    </p>
+    <p>This is currently only implemented for the Qt and GTK platforms.</p>
+
     <p>
      <b id="SC_ELEMENT_LIST">SC_ELEMENT_LIST : colouralpha</b><br />
      <b id="SC_ELEMENT_LIST_BACK">SC_ELEMENT_LIST_BACK : colouralpha</b><br />
diff --git a/gtk/PlatGTK.cxx b/gtk/PlatGTK.cxx
index 67b7f705..c7bcbb77 100755
--- a/gtk/PlatGTK.cxx
+++ b/gtk/PlatGTK.cxx
@@ -27,6 +27,9 @@
 #if defined(GDK_WINDOWING_WAYLAND)
 #include <gdk/gdkwayland.h>
 #endif
+#if GTK_CHECK_VERSION(3, 10, 0)
+#include <cairo/cairo-gobject.h>
+#endif
 
 #include "ScintillaTypes.h"
 #include "ScintillaMessages.h"
@@ -1432,6 +1435,7 @@ class ListBoxX : public ListBox {
 #if GTK_CHECK_VERSION(3,0,0)
 	std::unique_ptr<GtkCssProvider, GObjectReleaser> cssProvider;
 #endif
+	float imageScale;
 public:
 	IListBoxDelegate *delegate;
 
@@ -1439,7 +1443,7 @@ public:
 		pixhash(nullptr), pixbuf_renderer(nullptr),
 		renderer(nullptr),
 		desiredVisibleRows(5), maxItemCharacters(0),
-		aveCharWidth(1),
+		aveCharWidth(1), imageScale(1.0),
 		delegate(nullptr) {
 	}
 	// Deleted so ListBoxX objects can not be copied.
@@ -1673,7 +1677,13 @@ void ListBoxX::Create(Window &parent, int, Point, int, bool, Technology) {
 
 	/* Tree and its model */
 	GtkListStore *store =
-		gtk_list_store_new(N_COLUMNS, GDK_TYPE_PIXBUF, G_TYPE_STRING);
+		gtk_list_store_new(N_COLUMNS,
+#if GTK_CHECK_VERSION(3, 10, 0)
+		CAIRO_GOBJECT_TYPE_SURFACE,
+#else
+		GDK_TYPE_PIXBUF,
+#endif
+		G_TYPE_STRING);
 
 	list = gtk_tree_view_new_with_model(GTK_TREE_MODEL(store));
 	g_signal_connect(G_OBJECT(list), "style-set", G_CALLBACK(StyleSet), nullptr);
@@ -1701,7 +1711,13 @@ void ListBoxX::Create(Window &parent, int, Point, int, bool, Technology) {
 	gtk_cell_renderer_set_fixed_size(pixbuf_renderer, 0, -1);
 	gtk_tree_view_column_pack_start(column, pixbuf_renderer, FALSE);
 	gtk_tree_view_column_add_attribute(column, pixbuf_renderer,
-					   "pixbuf", PIXBUF_COLUMN);
+#if GTK_CHECK_VERSION(3, 10, 0)
+					   "surface",
+#else
+					   "pixbuf",
+#endif
+					   PIXBUF_COLUMN);
+
 
 	renderer = gtk_cell_renderer_text_new();
 	gtk_cell_renderer_text_set_fixed_height_from_font(GTK_CELL_RENDERER_TEXT(renderer), 1);
@@ -1915,11 +1931,20 @@ void ListBoxX::Append(char *s, int type) {
 		if (nullptr == list_image->pixbuf)
 			init_pixmap(list_image);
 		if (list_image->pixbuf) {
+#if GTK_CHECK_VERSION(3, 10, 0)
+			cairo_surface_t *surface = gdk_cairo_surface_create_from_pixbuf(list_image->pixbuf, imageScale, nullptr);
+			gtk_list_store_set(GTK_LIST_STORE(store), &iter,
+					   PIXBUF_COLUMN, surface,
+					   TEXT_COLUMN, s, -1);
+
+			const gint pixbuf_width = gdk_pixbuf_get_width(list_image->pixbuf) / imageScale;
+#else
 			gtk_list_store_set(GTK_LIST_STORE(store), &iter,
 					   PIXBUF_COLUMN, list_image->pixbuf,
 					   TEXT_COLUMN, s, -1);
 
 			const gint pixbuf_width = gdk_pixbuf_get_width(list_image->pixbuf);
+#endif
 			gint renderer_height, renderer_width;
 			gtk_cell_renderer_get_fixed_size(pixbuf_renderer,
 							 &renderer_width, &renderer_height);
@@ -2125,7 +2150,8 @@ void ListBoxX::SetList(const char *listText, char separator, char typesep) {
 	}
 }
 
-void ListBoxX::SetOptions(ListOptions) {
+void ListBoxX::SetOptions(ListOptions options_) {
+	imageScale = options_.imageScale;
 }
 
 Menu::Menu() noexcept : mid(nullptr) {}
diff --git a/include/Scintilla.h b/include/Scintilla.h
index 213dde52..a06f8a98 100644
--- a/include/Scintilla.h
+++ b/include/Scintilla.h
@@ -475,6 +475,8 @@ typedef sptr_t (*SciFnDirectStatus)(sptr_t ptr, unsigned int iMessage, uptr_t wP
 #define SCI_AUTOCGETMAXHEIGHT 2211
 #define SCI_AUTOCSETSTYLE 2109
 #define SCI_AUTOCGETSTYLE 2120
+#define SCI_AUTOCSETIMAGESCALE 2815
+#define SCI_AUTOCGETIMAGESCALE 2816
 #define SCI_SETINDENT 2122
 #define SCI_GETINDENT 2123
 #define SCI_SETUSETABS 2124
diff --git a/include/Scintilla.iface b/include/Scintilla.iface
index e1814eb5..7fde368f 100644
--- a/include/Scintilla.iface
+++ b/include/Scintilla.iface
@@ -1174,6 +1174,12 @@ set void AutoCSetStyle=2109(int style,)
 # Get the style number used for auto-completion and user lists fonts.
 get int AutoCGetStyle=2120(,)
 
+# Set the scale factor in percent for auto-completion list images.
+set void AutoCSetImageScale=2815(int scalePercent,)
+
+# Get the scale factor in percent for auto-completion list images.
+get int AutoCGetImageScale=2816(,)
+
 # Set the number of spaces used for one level of indentation.
 set void SetIndent=2122(int indentSize,)
 
diff --git a/include/ScintillaCall.h b/include/ScintillaCall.h
index b3197872..a2387f87 100644
--- a/include/ScintillaCall.h
+++ b/include/ScintillaCall.h
@@ -324,6 +324,8 @@ public:
 	int AutoCGetMaxHeight();
 	void AutoCSetStyle(int style);
 	int AutoCGetStyle();
+	void AutoCSetImageScale(int scalePercent);
+	int AutoCGetImageScale();
 	void SetIndent(int indentSize);
 	int Indent();
 	void SetUseTabs(bool useTabs);
diff --git a/include/ScintillaMessages.h b/include/ScintillaMessages.h
index d7ec27c3..67b4f69c 100644
--- a/include/ScintillaMessages.h
+++ b/include/ScintillaMessages.h
@@ -249,6 +249,8 @@ enum class Message {
 	AutoCGetMaxHeight = 2211,
 	AutoCSetStyle = 2109,
 	AutoCGetStyle = 2120,
+	AutoCSetImageScale = 2815,
+	AutoCGetImageScale = 2816,
 	SetIndent = 2122,
 	GetIndent = 2123,
 	SetUseTabs = 2124,
diff --git a/qt/ScintillaEditBase/PlatQt.cpp b/qt/ScintillaEditBase/PlatQt.cpp
index 91edf075..38aec01a 100644
--- a/qt/ScintillaEditBase/PlatQt.cpp
+++ b/qt/ScintillaEditBase/PlatQt.cpp
@@ -40,6 +40,7 @@
 #include <QTextLayout>
 #include <QTextLine>
 #include <QLibrary>
+#include <QtMath>
 
 using namespace Scintilla;
 
@@ -994,6 +995,7 @@ private:
 	bool unicodeMode{false};
 	int visibleRows{5};
 	QMap<int,QPixmap> images;
+	float imageScale{1.0};
 };
 ListBoxImpl::ListBoxImpl() noexcept = default;
 
@@ -1034,10 +1036,11 @@ void ListBoxImpl::Create(Window &parent,
 	int maxIconWidth = 0;
 	int maxIconHeight = 0;
 	foreach (QPixmap im, images) {
-		if (maxIconWidth < im.width())
-			maxIconWidth = im.width();
-		if (maxIconHeight < im.height())
-			maxIconHeight = im.height();
+		im.setDevicePixelRatio(imageScale);
+		if (maxIconWidth < im.width() / im.devicePixelRatio())
+			maxIconWidth = im.width() / im.devicePixelRatio();
+		if (maxIconHeight < im.height() / im.devicePixelRatio())
+			maxIconHeight = im.height() / im.devicePixelRatio();
 	}
 	list->setIconSize(QSize(maxIconWidth, maxIconHeight));
 
@@ -1085,8 +1088,8 @@ int ListBoxImpl::CaretFromEdge()
 	ListWidget *list = GetWidget();
 	int maxIconWidth = 0;
 	foreach (QPixmap im, images) {
-		if (maxIconWidth < im.width())
-			maxIconWidth = im.width();
+		if (maxIconWidth < im.width() / im.devicePixelRatio())
+			maxIconWidth = im.width() / im.devicePixelRatio();
 	}
 
 	int extra;
@@ -1165,9 +1168,9 @@ void ListBoxImpl::RegisterQPixmapImage(int type, const QPixmap& pm)
 	ListWidget *list = GetWidget();
 	if (list) {
 		QSize iconSize = list->iconSize();
-		if (pm.width() > iconSize.width() || pm.height() > iconSize.height())
-			list->setIconSize(QSize(qMax(pm.width(), iconSize.width()),
-						 qMax(pm.height(), iconSize.height())));
+		if (pm.width() / pm.devicePixelRatio() > iconSize.width() || pm.height() / pm.devicePixelRatio() > iconSize.height())
+			list->setIconSize(QSize(qMax(qFloor(pm.width() / pm.devicePixelRatio()), iconSize.width()),
+						 qMax(qFloor(pm.height() / pm.devicePixelRatio()), iconSize.height())));
 	}
 
 }
@@ -1226,8 +1229,9 @@ void ListBoxImpl::SetList(const char *list, char separator, char typesep)
 		Append(startword, numword?atoi(numword + 1):-1);
 	}
 }
-void ListBoxImpl::SetOptions(ListOptions)
+void ListBoxImpl::SetOptions(ListOptions options_)
 {
+	imageScale = options_.imageScale;
 }
 ListWidget *ListBoxImpl::GetWidget() const noexcept
 {
diff --git a/src/AutoComplete.cxx b/src/AutoComplete.cxx
index 2487d304..058550d6 100644
--- a/src/AutoComplete.cxx
+++ b/src/AutoComplete.cxx
@@ -41,6 +41,7 @@ AutoComplete::AutoComplete() :
 	ignoreCase(false),
 	chooseSingle(false),
 	options(AutoCompleteOption::Normal),
+	imageScale(1.0),
 	posStart(0),
 	startLen(0),
 	cancelAtStartPos(true),
diff --git a/src/AutoComplete.h b/src/AutoComplete.h
index 378232a8..b1233332 100644
--- a/src/AutoComplete.h
+++ b/src/AutoComplete.h
@@ -25,6 +25,7 @@ public:
 	bool ignoreCase;
 	bool chooseSingle;
 	AutoCompleteOption options;
+	float imageScale;
 	std::unique_ptr<ListBox> lb;
 	Sci::Position posStart;
 	Sci::Position startLen;
diff --git a/src/Platform.h b/src/Platform.h
index 77464945..ccd073af 100644
--- a/src/Platform.h
+++ b/src/Platform.h
@@ -317,6 +317,7 @@ struct ListOptions {
 	std::optional<ColourRGBA> foreSelected;
 	std::optional<ColourRGBA> backSelected;
 	AutoCompleteOption options=AutoCompleteOption::Normal;
+	float imageScale;
 };
 
 class ListBox : public Window {
diff --git a/src/ScintillaBase.cxx b/src/ScintillaBase.cxx
index a95df76a..5529d3ba 100644
--- a/src/ScintillaBase.cxx
+++ b/src/ScintillaBase.cxx
@@ -270,6 +270,7 @@ void ScintillaBase::AutoCompleteStart(Sci::Position lenEntered, const char *list
 		vs.ElementColour(Element::ListSelected),
 		vs.ElementColour(Element::ListSelectedBack),
 		ac.options,
+		ac.imageScale,
 	};
 
 	int lineHeight;
@@ -956,6 +957,12 @@ sptr_t ScintillaBase::WndProc(Message iMessage, uptr_t wParam, sptr_t lParam) {
 	case Message::AutoCGetStyle:
 		return vs.autocStyle;
 
+	case Message::AutoCSetImageScale:
+		ac.imageScale = static_cast<int>(wParam) / 100.0;
+
+	case Message::AutoCGetImageScale:
+		return ac.imageScale * 100;
+
 	case Message::RegisterImage:
 		ac.lb->RegisterImage(static_cast<int>(wParam), ConstCharPtrFromSPtr(lParam));
 		break;
diff --git a/test/simpleTests.py b/test/simpleTests.py
index 867c0ebb..0389d6db 100644
--- a/test/simpleTests.py
+++ b/test/simpleTests.py
@@ -3188,6 +3188,7 @@ class TestAutoComplete(unittest.TestCase):
 		self.assertEqual(self.ed.AutoCGetIgnoreCase(), 0)
 		self.assertEqual(self.ed.AutoCGetAutoHide(), 1)
 		self.assertEqual(self.ed.AutoCGetDropRestOfWord(), 0)
+		self.assertEqual(self.ed.AutoCGetImageScale(), 100)
 
 	def testChangeDefaults(self):
 		self.ed.AutoCSetSeparator(ord('-'))
@@ -3222,6 +3223,10 @@ class TestAutoComplete(unittest.TestCase):
 		self.assertEqual(self.ed.AutoCGetStyle(), 13)
 		self.ed.AutoCSetStyle(self.ed.STYLE_DEFAULT)
 
+		self.ed.AutoCSetImageScale(200)
+		self.assertEqual(self.ed.AutoCGetImageScale(), 200)
+		self.ed.AutoCSetImageScale(100)
+
 	def testAutoShow(self):
 		self.assertEqual(self.ed.AutoCActive(), 0)
 		self.ed.SetSel(0, 0)
-- 
2.47.0
auto-c-image-scale.zip (application/zip, 254.1 KB) - not displayed