Text Engine is a rich-text editing framework for GTK 4. The primary user
of this library is bluetype but it
can be used wherever rich text display and editing is needed.
Status
The library is under heavy development and generally not suitable for
use in applications. For packagers, note that Extension Manager builds
against version 0.1 of this library.
Matrix
Development of text-engine and bluetype takes place on matrix.
Join #bluetype to chat
about the project or if you would like to get involved. Come say hello!
Licence
Text Engine is dual-licensed under the Mozilla Public License 2.0 and
the GNU Lesser General Public License 2.1 (or any later version), at your
option. You may choose to use the library under either of the aforementioned
licenses, or both. See COPYING for more information.
The text-engine rendering model is a very simplified version of most major browsers:
It consists of five individual trees:
Model: A hierarchical description of the document (basically HTML with fewer elements)
Format: A cascade of styles (CSS circa v2?)
Style: Matching model elements with their relevant styles to construct an efficient representation of the fully formatted document
Layout: Recursively determines how large elements want to be, what size they should be (in relation to the available space), etc. The layout module is designed to be hotswappable by the end user, so they can customise text-engine to their liking (the other models are likely to be internal only). Different layouts can be used to achieve things like reflowable vs paged layouts.
Display: Convert layout nodes to platform specific drawing commands. Also responsible for things like occlusion culling, etc. GTK code may exist ONLY in this stage. This would also let us support e.g. Qt, GTK3 if someone shows up with patches.
Each tree is cached and only updates relevant parts whenever a previous tree is invalidated (see #11). Style nodes and layout nodes will be attached together; there is a one-to-one relationship between them. Style nodes wrap model nodes and index format nodes. I might simplify this by dropping style altogether and make model nodes refer to format directly.
Unfortunately unicode text handling is rather painful in C. Additionally, being glib-based causes all sorts of problems for embedding due to the LGPL forbidding static linking.
Investigate a rewrite of the library in Rust, using pluggable backends for font metrics and a custom text layout system (we handling line breaking, justification, etc instead of delegating to Pango).
Once #23 is finished, we'll have enough in-place to display most simple epub2 books (minus images). Could add an epub reader demo to show off the bells and whistles.
Hi, @mjakeman I'd like to package this for debian but I see you've changed the licence after the v0.1.0 release. So would you mind making a new release so I can package it with just the LGPL?
aka 'The wonderful wild world of word wrapping' :/
Introduction
The way we currently handle cursor positions is a mess. For each paragraph, it is comprised of several 'runs', with each run being a block of contiguously formatted text. Text is indexed on a per-paragraph basis.
Character movement is handled by the TextEditor class, while home/end movement is handled by the TextDisplay class. This is because at present a TextDocument is a semantic description of the document, paired with a TextLayout to create the actual formatted document. TextEditor operates directly on the semantic document (as it should, at least for now).
Traversal between paragraphs is not a problem as each paragraph can be considered a 'self-contained' block, so going from the end of one paragraph to the start of another paragraph is one movement backwards/forwards.
The problem arising when dealing with paragraphs that span multiple lines, and particularly when words themselves are split instead of wrapped. When we use home/end, where should the cursor go?
Google Docs:
Paragraph wraps, word is not split:
The space is used to correctly handle traversal between the end of the first line and the start of the second. No special case needed.
Screen.Recording.2022-08-24.at.5.54.53.PM.mov
Paragraph wraps, word is split:
Docs appears to use a 'before character' approach in that the caret position is determined by the following character. This is particularly nice because pressing end takes you to the final index position on the line, belonging to the final character on the line (i.e. the space or tab). For the final line in the paragraph however, there is no break and so we need to account for the extra index.
Screen.Recording.2022-08-24.at.6.07.59.PM.mov
Text Engine:
Paragraph wraps, word is not split:
Normal navigation works, but the current state of home/end dumps the cursor on the line after. This is probably fixable by choosing the index preceding the final character on the line.
Screen.Recording.2022-08-24.at.6.00.54.PM.mov
Paragraph wraps, word is split:
Again this works similarly to Google Docs, however we have the additional '-' character inserted by Pango when word wrapping. We could probably disable this? The issue here which makes it look much worse than it is stems from 'end' using the character after, rather than the character before. This makes it appear like two characters are being skipped. Switching to a character before system puts us on par with Google Docs and is probably the best path forward:
Screen.Recording.2022-08-24.at.6.03.44.PM.mov
Again we need to count for the additional index on the final line, as there is no 'break' character.
Conclusions
We should:
Follow the Google Docs model of character positioning within paragraphs
Make Home and End to use the first index and last index on a given line
Refactor movement into a new TextTraversal auxiliary object?
Alternatively make TextEditor layout-aware (not too happy with the idea)
As of #32, we use unicode character offsets exclusively for navigation.
Character offsets refer to unicode code points, however some characters like emoji are composite and use multiple code points. Let's find a way to navigate by grapheme (group of code points) rather than each code point.
I'm interested in building a rich text notes app using GTK and python. With python you use https://pygobject.readthedocs.io/en/latest/ to create GTK apps and i'm not sure how to incorporate this one.
Several files' compilation will trigger -Wincompatible-pointer-types diagnostics. This will prevent the project from being successfully built on Fedora 40 and other GNU/Linux distributions that use GCC 14 because, on GCC 14, -Wincompatible-pointer-types is treated as an error rather than a warning.
Steps to reproduce:
Clone this repository and enter the directory that contains the clone.
If gcc --version reports GCC 14 or above, then run meson setup build;
Otherwise, run CFLAGS="-Werror=incompatible-pointer-types" meson setup build.
There's a very noticeable bug with the current implementation of paragraph breaks where the cursor cannot be placed on the final index of a paragraph using the mouse or up/down navigation. It can however be done using home/end.
This is due to Pango's internal handling of paragraphs, but isn't something we can easily work around. It might be an idea to use an invisible paragraph break character (I'm sure there's something in unicode) and drop our special casing altogether.
text-engine uses multiple mutable trees in order to store semantic, style, layout, and render states.
The current pipeline (without a style module) looks as follows:
Model --> Layout --> Render
At present, we recreate the entire layout tree each time a draw is queued. Instead, we should cache the layout tree between redraws. Whenever a change is made to the model, the changed element should be marked as dirty (full recalculation) and all parent elements should be marked as having dirty descendants (partial recalculation).
The layout object watches for changes to the root element and triggers a partial recalculation whenever the root element is either marked dirty or has dirty descendants. Layout recalculations can be done in parallel, as we assume a normal flow for the document (this may change if floating images are supported). If there are more than N recalculations in a frame, we should mark the entire tree as dirty.
Something to avoid is recalculating layout every time the cursor and/or selection changes. This ties in with needing to support Home/End keyboard shortcuts on a per-line basis, as the length of a line is determined by the layout process rather than the document model itself. We'll need some way of moving from Model -> Layout efficiently in order to retrieve line layout data.
Cursor movement is currently split between editor.c and display.c which is very undesirable. Movement should be handled at a single location, preferably at a level just above layout - it is layout dependent.
The cursor and selection should be separated from the general idea of 'marks' (i.e. cursor/selection is a mark, but not all marks are cursors). We'll also want some way to index text without using marks, in the case of temporary or read only access to the document model.
Implement 'replaceable' items where the actual drawing (and possibly layout) is done externally. This allows for images and custom controls e.g. image handles, cropping, etc.