Newsletter, March 2023
Color Emoji, Text Truncation, and More

This month saw the merge of two notable text features: color bitmap glyph support and automatic shaper-driven text truncation. With these features, we are now able to display color emoji, and we are also able to visually indicate when text with a configured maximum number of lines has been truncated. I’d like to thank Plato Team for supporting this work financially, and Elias for reviewing the mountains of patches it generated.

Additionally many folks in the community contributed features and bugfixes. Thanks everyone!

Sponsorship

This month, Gio thanks the following organizations and community members for their ongoing support!

Supporting the whole team:

Supporting a maintainer:

Sponsorship money given to Gio enables Elias and I to cover the costs of running Gio’s infrastructure, to pay for some of our time spent maintaining and improving Gio, and to plan for funding significant feature work. You can support Gio by contributing on OpenCollective or GitHub Sponsors.

Changes by repo

Below you can find summaries and details of all changes across the official project repositories.

core gio

As discussed above, core saw some major changes to the text stack. That resulted in a few breaking changes in the exported API, but the many applications will be unaffected.

Shaper API Changes

If you directly invoke text.Shaper.Shape to get the clip.PathSpec of text, you’ll now need to also invoke text.Shaper.Bitmaps with the same []text.Glyph to get an op.CallOp containing bitmap glyphs. Proper use looks something like this:

// Assuming we have a slice of glyphs:
var line []text.Glyph
path := shaper.Shape(line)
outline := clip.Outline{Path: path}.Op().Push(gtx.Ops)
// set the paint material here.
material.Add(gtx.Ops)
paint.PaintOp{}.Add(gtx.Ops)
outline.Pop()
// Paint any bitmaps here. Notably, this callOp may change the
// paint material, so it's important to always set the proper
// paint material before painting the text above.
if call := shaper.Bitmaps(line); call != (op.CallOp{}) {
	call.Add(gtx.Ops)
}

Widget API Changes

If you use the widget API for displaying text, there are a few changes that affect you:

Grapheme Cluster Support

All interactive text widgets now expose their APIs in terms of “grapheme clusters,” not runes. A “grapheme cluster” is Unicode’s attempt to standardize a collection of potentially many runes that a use perceives as a single character. Supporting this is critical for displaying many non-latin languages, and also for emoji (which are often built of quite a few different runes). Applications with sophisticated editor content manipulation should start checking the final rune positions of cursors after programmatically moving the cursor, as the final positions will be clamped to grapheme cluster boundaries.

Label Changes

widget.Label is now always non-interactive. Before it had a Selectable field you could set to make it interactive, but this added complexity and made its Layout signature more complicated. Its Layout method now looks like this:

func (l Label) Layout(gtx layout.Context, lt *text.Shaper, font text.Font, size unit.Sp, txt string, textMaterial op.CallOp) layout.Dimensions {

The textMaterial parameter is expected to set a paint material for the text. If you wanted to paint black text, you could construct the textMaterial like so:

macro := op.Record(gtx.Ops)
paint.ColorOp{Color: color.NRGBA{A:255}}.Add(gtx.Ops)
textMaterial := macro.Stop()

You can, of course, also use a paint.ImageOp or paint.LinearGradientOp.

Selectable Changes

widget.Selectable is now used to display interactive text instead of widget.Label. Its Layout method looks similar to widget.Label, except that it accepts two materials, one for the text and one for the text selection rectangle.

func (l *Selectable) Layout(gtx layout.Context, lt *text.Shaper, font text.Font, size unit.Sp, textMaterial, selectionMaterial op.CallOp) layout.Dimensions {

If you display text with a maximum number of lines configured via any of

  • text.Parameters.MaxLines,
  • widget.Label.MaxLines,
  • or material.LabelStyle.MaxLines

you can now also supply a Truncator string to display if the string wasn’t able to fit within the available lines. This truncator will default to if not specified, and cannot be turned off. If interactive text is truncated, selecting the truncator symbol is logically equivalent to selecting the entire truncated text, so users can copy&paste the full content from a truncated label.

Using widget.Selectable.Truncated() it is now possible to create widgets that display truncated text, add their own hover detection, and choose a way to display the un-truncated text when hovered.

I’ve added gioui.org/example/textfeatures to demonstrate color glyph rendering and custom truncator strings.

Other Changes

Additionally, Serhat Sevki Dincer contributed some great code cleanups, Dominik Honnef improved layout.List’s scrolling performance and widget.Image’s scaling behaviors, and Larry Clapp fixed a bug in font fallback that could choose the wrong face for displaying strings.

Breaking changes by author:

Chris Waldon

  • font/opentype,text,widget{,/material}: [API] support bitmap glyph rendering. This commit supports rendering opentype glyphs containing bitmap data instead of color data. In order to support returning the shaped bitmap glyphs from the Shaper’s Shape() method, it has gained a second return parameter, an op.CallOp. Adding that CallOp immediately after or immediately before painting the returned path will display the bitmap glyphs. 6ab3ff40
  • widget: [API] implement UAX#29 grapheme clustering in text widgets. This commit teaches the text widgets how to position their cursor according to grapheme cluster boundaries rather than rune boundaries. While this is more work, the results better match the expectations of users. A “grapheme cluster” is a user-perceived character that may be composed of arbitrarily many runes. 5c54268d
  • widget{,/material}: [API] split interactive and non-interactive text widgets. This commit separates the types for interactive and non-interactive text within package widget. widget.Selectable is used for all interactive text. widget.Label is used for all non-interactive text. There is no longer a field on widget.Label to provide it with a Selectable. If you want selectable text and are not relying upon the material pacakge API, you need to create widget.Selectables instead of widget.Labels. The material package’s LabelStyle API is unchanged. 9d0a53fc
  • text,widget{,/material}: [API] move all shaping parameters into text.Parameters. This commit moves the min/max width of shaped text and the text’s Locale into text.Parameters. They were previously passed as separate function parameters to the shaper, but this made little sense and added visual noise. This is a breaking change, but only if you previously invoked the shaping API directly. 7e8c1092

Non-breaking changes by author

Chris Waldon:

  • text: test maxlines with exported API. This commit changes how the test for line wrapping is implemented to rely on the exported API rather than internal symbols. 1210bbb3
  • widget: ensure proper modifiers on key events. This commit extends the key event handling for text widgets to always check for appropriate modifier keys. Previously this wasn’t necessary, as the text widgets would only ever receive key events it registered for, but now it may be the top-level key event handler and thus receive all key events that aren’t handled elsewhere. b09ef80d
  • layout: ensure Spacer obeys constraints. This commit ensures that the Spacer type doesn’t break layouts by ignoring when its min constraints require it to be larger or its max constraints require it to be smaller. d7b1c7c3
  • go.*,font/opentype,text: switch to latest go-text/typesetting api. This commit upgrades our go-text version to the latest one which internalizes harfbuzz and supports text truncators. This allows us to drop our dependency upon Benoit’s textlayout package. 47d25c13
  • go.*,widget: add initial emoji rendering benchmarks. This commit upgrades our version of eliasnaur.com/font to include a color emoji font and uses that to benchmark displaying large quantities of emoji. As expected, this is very slow when the strings change frequently, and uses silly amounts of memory. Future commits will work to improve this. 3bdbcab8
  • text: cache bitmap glyph image operations. This commit adds caching to the process of extracting bitmap images from glyphs, ensuring that we only do so once for a given glyph so long as it isn’t evicted from our LRU. 25171df6
  • widget: make glyphIndex reusable. This commit allows the glyph index type to be reset and reused, preventing the reallocation of numerous buffers when indexing glyphs. 36e768e7
  • go.*,text,widget{,/material}: implement text truncators. This commit adds support for the idea of a text “Truncator”, a string that is shown at the end of truncated text to indicate that it has been shortened because it would not fit within the requested number of lines. 959f5889
  • widget: expose truncation status of Selectable. This commit adds an exported method to enable widgets to detect when the text displayed by a Selectable has been truncated. This can be used to implement proper show-full-text-in-an-overlay behavior in a parent widget. I haven’t attempted to implement that in core yet, as it is a complex feature involving animation and pointer interaction. 5e6e1217
  • widget/material: export LabelStyle.Shaper and document fields. We panic when someone constructs a literal LabelStyle because they cannot possibly populate the shaper field. The resulting error is cryptic, and unusual within Gio because most style types are safe to construct literally. This commit enables creating literal LabelStyles by exporting the Shaper field, and also documents the purposes of all of the fields. 5a404816

Serhat Sevki Dincer:

  • app,gpu{,/headless,/internal/rendertest}: replace io/ioutil with io & os. 39b11584
  • text,widget: remove ineffective assignments. 35a82319
  • text: simplify font weights. 4a1962e5

Dominik Honnef:

  • layout: simplify implementation of List.ScrollTo. dc9a4a40
  • layout: improve documentation for List.ScrollTo and List.ScrollBy. 107401cf
  • widget: [API] correct default scaling of images. When no scale factor is set, scale by 1.0, mapping one image pixel to one device-independent pixel. This matches the behavior of CSS and other frameworks. 51b11486

Larry Clapp:

  • text: fix sorting in faceOrderer.sorted. faceOrderer.sorted tried to put the “primary” font first by tweaking the “less” function in sort.Slice, but it didn’t work correctly. fa34121f

gio-x

X saw a major overhaul of Windows notification support by Jack Mordaunt, a missing feature added to component surfaces by Lothar May, and a bugfix to text fields by Gordon Klaus. Thanks everyone!

Chris Waldon:

  • go.*,styledtext: update to new text shaper API. fdc1b67
  • go.*: update to latest gio. fd712aa

Lothar May:

  • component: add Fill color field to SurfaceStyle. 05b40af

Jack Mordaunt:

  • notify: [windows] use COM based toast notifications. This commit changes the toast dependency to one with a similar api but uses COM directy instead of powershell. aad49f4

Gordon Klaus:

  • component: lay out TextField.Editor in a Flexed child. This positions Prefix and Suffix correctly (at the ends of the field) and stretches the editor as wide as possible so it accepts click events in a larger area. 64527da

gio-example

As discussed above, I added a new text features example.

Chris Waldon:

  • go.*: update to latest gio and gio-x. d22df62
  • textfeatures: add simple demo for color glyphs and truncation. This commit adds a simple program demonstrating how to load a color emoji font, how to configure a custom text truncator, and how the truncator behaves at various max widths. 059eaf6

End

Thanks for reading!

Chris Waldon