Hello,
I'm writing to announce that I have developed a mostly working port of
Scintilla to the GTK 4 toolkit.
## Why?
GTK 3 is in maintenance mode: there are no new developments, it's API
and feature frozen. It only receives small fixes, sometimes. GTK 4 is
what is being actively developed and used by most applications. It's
modern and cool and featureful.
Geany maintainers have expressed interest in a GTK 4 port of Geany,
but Scintilla is the biggest missing piece [0]. There was also a post
by Neil on this mailing list suggesting that this is desirable [1].
[0]:
https://github.com/geany/geany/discussions/3675
[1]:
https://groups.google.com/g/scintilla-interest/c/RX4kQkWJBk8
I am involved in a project to develop a large commercial application,
using GTK 4 as the toolkit. The application uses Scintilla; so I took
up the challenge of making the port.
## What It Is and Does
As suggested by Neil in [1], this is not an adaptation of the GTK 2/3
port (in gtk/ of the source tree), but rather a whole new independent
port, living in gtk4/. It doesn't directly use code form the GTK 2/3
port, although I did consult that port for ideas & inspiration when
working on the GTK 4 one.
Another bit of motivation for structuring this as a separate port is
that I wanted to take the opportunity to do the GTK 4 version quite
differently from the old one, in various aspects. That is, given my
background in GTK and GObject (as opposed to background in Win32 and
Scintilla internals, which I imagine other Scintilla developers might
have), I wanted to do things in a way that is much more *proper* and
*native* for GTK and GObject. I wanted Scintilla to be a "good
citizen" of the GTK ecosystem, to behave a lot like any other GTK
("self-respecting", if you will) widget and library would.
I tried to keep the port self-contained, i.e. it only adds a gtk4/
directory, without altering the existing code in src/ and elsewhere.
That being said, this required some compromises (read: hacks), and
some things would be cleaner if we could tune some Scintilla internals
to better suit the needs of the GTK 4 port.
## The Widget
There is a single widget, ScintillaView. It implements the
GtkScrollable interface [2]. It doesn't contain the scrollbars or
handle the scrolling events/gestures, instead you're expected to place
a ScintillaView as a child into GtkScrolledWindow [3], which is what
manages scrollbars and events; this is how scrollable widgets are done
in GTK.
[2]:
https://docs.gtk.org/gtk4/iface.Scrollable.html
[3]:
https://docs.gtk.org/gtk4/class.ScrolledWindow.html
The ScintillaView class has a bunch of GObject *properties*, as well
as widget *actions*, this is again similar to what all the other GTK
widgets are doing.
## Rendering Concerns
GTK 4 has a whole new rendering model, which is based on a scene
graph, a tree of render nodes, and GPU-based rendering (potentially,
with the actual rendering happening on a separate thread concurrently
to the application logic, but this is not implemented in GTK yet).
This is in contrast to GTK 3, which used a more classic CPU/RAM-based
rendering model utilizing Cairo, a 2D graphics library with a
plotter/PostScript-like API.
It is technically possible to still use Cairo in GTK 4 through
GskCairoNode / gtk_snapshot_append_cairo, which will cause GTK to
render the 2D graphics on the CPU / in the RAM, then upload the
resulting texture to the GPU. But this has all the downsides of the
CPU-based/2D rendering (performance on HiDPI). So the recommendation
is to use GTK4-native rendering as much as possible, and only to fall
back onto Cairo when that is not possible.
Fortunately, in most cases, Scintilla's drawing operations (the
Surface class) map fairly well to that of GTK 4! So most of the
drawing Scintilla does ends up as fairly idiomatic render nodes in the
scene graph. There are some unfortunate mismatches though, of which I
could talk more about.
GTK supports HiDPI displays, with both integer and fractional (e.g.
2.5x) scaling. This is, for the most part, transparent to
applications; all the coordinates and sizes on the GTK level are
expressed in logical units, which are converted to and from *device
pixels* at a lower level. The render tree (that is produced by the
widget tree of a window) is essentially a piece of vector graphics,
which is then rendered for the desired physical resolution, at an
appropriate scale. But there are some considerations for applications
& widgets as well: in particular Scintilla tries to round extents to
physical pixels via a PixelDivisions value, which is, for one thing,
an integer, with no way to represent a fractional scale like 2.5 or
1.75. Even if it was made a float, there is still no clear way how
this could work, because the "subpixel positions" of device pixels are
*not* uniform among the "logical pixels".
Another issue here is that Scintilla paints the background behind each
piece of text individually as its own little rectangle; this for one
thing has the downside of producing a lot of render nodes (a lot of
small white rectangles, instead of one large white rectangle covering
the whole widget area), and therefore a bunch of extra work for the
GPU to push through. But also visually, while with integer scaling the
sides of the rectangles align perfectly and the whole background ends
up painted white, with fractional scaling the sides do not always
align, and there are visible "seams". This is a general issue with
fractional scaling, but it's aggravated by what Scintilla is doing
(drawing these little rectangles and expecting them to align), and
typically avoided by just not doing that. I tried to make it less bad
by setting the default background color to transparent, and instead
painting the white background the typical GTK way (adding the CSS
.view class); this helps a bunch, but seams still show up as soon as
you e.g. select some text.
There is some work on the GTK side [4] to enable opt-in *snapping* of
render nodes to physical pixels, whether by growing or shrinking or
rounding rectangles a tiny bit to land precisely on physical pixel
boundaries, thus ensuring there are no seams between two neighboring
snapped rectangles. Once this work lands in a future version of GTK,
we should be able to make use of it in Scintilla.
[4]:
https://gitlab.gnome.org/GNOME/gtk/-/merge_requests/8494
Another topic here is what we even want to draw. Generally, GTK
doesn't define a global set of things like "the platform highlight
color" or "the default font". Instead, widgets are assigned *style*
via *stylesheets* (written in CSS). This style is dynamic, so it
varies with time (the colors change when the system-wide dark mode is
toggled, for example), and between widgets -- so different instances
of ScintillaView could have different styles assigned to them,
depending on the context where they appear. So it makes no sense to
talk about which font should be used, for example, without mentioning
which specific widget that question is about.
I had used various tricks to somewhat reconcile Scintilla code's idea
of each platform having a single global style with this GTK's idea of
style inherently coming from a style sheet and differing per-widget
and with time. It works somewhat, but it's not as perfect as it could
be.
Yet another topic is phases. In GTK, "every frame is perfect". We
don't draw bad frames, because... why would we do such a thing? But
Scintilla sometimes decides to just *adandon painting* if it discovers
it needs to re-style or update something in the middle of painting. I
must admit, I don't quite understand what is supposed to happen in
this case on other platforms (a brief flash of black? a bunch of stale
pixels from a previous frame?), but this is not an option for GTK; we
must do things properly. One option here would be immediately starting
a new paint cycle if Scintilla core decides to abandon painting,
hoping that this second one succeeds. This would require creating an
auxiliary GtkSnapshot object, and still wouldn't solve the issue
entirely, since the changes that Scintilla core may do, such as
updating positions of scroll bars, really need to happen before the
*drawing phase*, namely in the *layout phase* of the GDK frame clock
[5]; various things in GTK rely on various other things being done in
the correct phase, and this whole mechanism is intentionally designed
in such a way that we can do things in the right order and display
every frame perfectly. I ended up pushing a bunch of work that *would*
cause painting to be abandoned into the size-allocate virtual method;
this appears to work great this far.
[5]:
https://docs.gtk.org/gtk4/drawing-model.html#the-frame-clock
## GObject Introspection
GObject introspection [6] is a cool feature of the GObject type system, where:
* a library's APIs are annotated in a special way in the source code;
* a tool called g-i-scanner scans the source code, the annotations,
and the compiled library, and produces a structured, machine- and
human-readable description of the API (in XML and binary formats);
* based on that API description, pretty documentation can be generated
using gi-docgen [7];
* bindings for using the library from many other languages (including
Python, JavaScript, C++, Rust, Go, C#/.NET, Java, and others) can be
generated automatically and transparently, which enables using the
library almost seamlessly from pretty much any language.
[6]:
https://gi.readthedocs.io/
[7]:
https://gnome.pages.gitlab.gnome.org/gi-docgen/
Of course, all of these goodies are only available if the library
exposes a C API in the GObject style, and is properly annotated.
There are some bits of this in the GTK 2/3 port already, but for the
GTK 4 port, I went all in. Various things that, on Win32, are
available through the messaging model, are exposed as GObject
properties, various other things as GObject methods, with annotations
matching the Scintilla semantics.
For example, SCI_APPENDTEXT is exposed as scintilla_view_append_text()
in C, which turns into a Scintilla.View.append_text() method in the
object model, which can be then called from JavaScript just like so:
```
import Scintilla from 'gi://Scintilla';
import Gtk from 'gi://Gtk?version=4.0';
Gtk.init();
const sc = new Scintilla.View();
sc.append_text("Hello");
```
In some cases, this work has uncovered issues in the GObject
introspection itself [8].
[8]:
https://gitlab.gnome.org/GNOME/gobject-introspection/-/issues/536#note_2398113
Scintilla has accumulated a lot of API surface; I haven't completed
exposing all of it as GObject methods & properties -- yet. But this
looks very nice & very promising in places where I have done the job.
One little thing I'm unreasonably proud of is that
SCI_GETDOCPOINTER/SCI_SETDOCPOINTER are exposed as a property (and
accessor methods) of type ScintillaDocument. When compiling in C++
mode, ScintillaDocument is just a type alias for
Scintilla::IDocumentEditable, but in C it's an opaque type. This type
is then registered as a "boxed type" in the GObject type system, and
appears as just a normal *boxed record* type when seen through
introspection (and so from language bindings), but internally it
really is an IDocumentEditable.
The classic "send message" funnel is also exposed as
scintilla_view_send_message, but it is not at all
introspection-friendly, so it's cumbersome to use from anything but
C/C++/Objective-C.
## Shared Library
Scintilla is built as a shared dynamic library. All of these C
functions are exposed from it; it is expected that users will link it
just like they link any other library (including GTK itself), and call
the provided APIs, either directly via linker-assisted symbol
references (from static languages), or by dynamically looking up
symbols (which is what happens when you invoke an API from a dynamic
language like Python or JavaScript).
It is also possible to use Scintilla in the classic style, by *not*
linking to the library, dlopen'ing it at runtime, and then finding a
few functions (you can get by with as little as two, namely
scintilla_view_new and scintilla_view_send_message). But this is again
only really remotely nice to do from C.
I have spent some effort on ensuring proper symbol visibility, so only
the symbols that are intended to be exposed from the shared library,
are, and the rest are internal to the library and are indeed
referenced as such (by a PC-relative offset and not via GOT or PLT). I
use both symbol visibility annotations and a linker script to achieve
this.
It would also be great if distributions (like Debian etc) packaged
Scintilla as a normal library, much like GTK or any other, with the
compiled library appearing in /lib/libscintilla.so (or such), the
headers at /usr/include/ScintillaView.h, and so on. To that end, we
might want to write and install a pkg-config file for Scintilla; this
would enable others to link to the system build of Scintilla easily,
e.g. with just `dependency("scintilla")` in Meson.
## Status
Works:
* Basic functionality: you can view and edit text documents
* Scrolling, including kinetic scrolling on touchpads and touchscreens
(thanks to GtkScrolledWindow)
* GObject introspection, using the library from various languages
* Lexilla, syntax highlighting, folding
* Clipboard: you can cut/copy/paste pieces of text. When you copy text
from one instance of Scintilla to another (or the same one) inside one
application, the selection is not serialized to a string, but is
represented by the SelectionText type, and keeps the rectangular shape
etc. Support for the primary clipboard (middle-click/triple-tap paste)
is halfway-implemented: you can paste text copied from elsewhere, but
Scintilla itself doesn't yet claim the primary selection.
There is some support for:
* IME (e.g. on-screen keyboard)
* Accessibility, using GtkAccessible and GtkAccessibleText (when
building with GTK 4.14 or later) interfaces. If you activate Orca the
screen reader, it reads the text around cursor, the current selection
etc
* The context menu is there, but it's the native GTK menu, populated
via a menu model (GMenuModel), and not Scintilla core's idea of a menu
(AddToPopUp).
But, it is very likely that there are issues concerning encodings,
bytes vs characters vs something else. There is work to be done in
straightening this out.
Not implemented / does not work yet:
* Drag-n-drop of text
* Call tips
* Autocompletion popovers
* Some more arcane drawing operations
* Proper accent color / dark theme / high contrast support (this
requires Scintilla styles to better follow the GTK stylesheet)
* Non-UTF-8 support? (Is that a thing?)
* Various bidirectional text things are very likely broken
Touchscreen experience is not very perfect: scrolling with your finger
also causes cursor/selection jumps, there are no text handles
(analogous to GtkTextHandle), and as said IME is not quite working
perfectly either.
I have built & tested Scintilla on GNU/Linux (Wayland and X11) and
Windows, but not on other targets supported by GTK (macOS, Android,
Broadway).
Another TODO is commenting/documenting all of this code. This means
both documenting the public APIs (so the gi-docgen documentation is
complete and can be read instead of the Win32-message-oriented one
that's currently visible at [9]), and commenting things internally, so
it's very clear to any person looking to read or contribute to the GTK
4 port how things are done, what caveats and important points there
are.
[9]:
https://scintilla.org/ScintillaDoc.html
## The Demo
Included with the port is a little application called Scintilla Demo,
which is a very basic text editor, with tabs and menus and a
ScintillaView in each tab. It's using cool technologies like Vala and
Blueprint and Gio (but not libadwaita, as to not tie it to GNOME). It
is *very* basic and incomplete; for instance I implemented opening
files, but not saving them. I don't think it should turn into a
feature-complete competitor to GNOME Text Editor or Geany or SciTE;
it's fine if it stays very simple, though of course basic
functionality should be completed.
Speaking of SciTE, is there interest in porting that to GTK 4 (while
simultaneously making it a lot more idiomatic)? Last time I looked at
SciTE, I could not exactly figure out how to run it "uninstalled", how
all these "properties" are intended to work; but perhaps I should take
another look?
## Where Do We Go From Here?
I'm looking for people who are interested in Scintilla and GTK 4; if
you are, please let me know! This port could use some more testing,
and real-world usage beyond my own use cases. There is also still a
lot to work on (accessibility, IME support, call tips,
autocomplete...), let's hack on this together!
I'm looking for feedback from Scintilla core developers. Do you want
this to go upstream?
If so, how do we organize this process? I see you're using Git and
GitHub for Lexilla, but SourceForge and Mercurial for Scintilla. I'm
not familiar with those; would it work for you if I send you a patch
(in diff(1) / patch(1) / git format-patch -compatible format) or
upload a code archive or a Git repository somewhere? How do we do code
review?
Could we host the HTML documentation autogenerated using gi-docgen
somewhere on
scintilla.org? Is there a CI process?
Would you be open to changing those little things about Scintilla core
that would make the GTK 4 so much less hacky? (One example of this
might be, not querying a "platform color" globally, but in relation to
a specific instance of Scintilla.)
If you *don't* want this upstream, for one reason or another, are
people nevertheless interested in this work, should I maintain a
friendly downstream fork?
Cheers,
Sergey
P.S. This email was typed and edited using Scintilla on GTK 4, in the
Scintilla Demo application.