This commit updates the text package to be able to load system fonts. As a consequence,
application authors may choose to provide no fonts manually, and it's
also possible that the system provides none (WASM, for instance, currently provides no
system fonts). As such, the text stack needed some minor tweaks to handle this case by
displaying blank spaces where text should be rather than crashing when no faces are
available.
Internally, we are dropping the old method of choosing faces and instead relying solely
on the new font matching logic in go-text. I chose to do this because maintaining two
different sets of logic with a hierarchical relationship proved to be really complex,
and also the go-text logic seems to produce higher-quality choices.
The breaking API change from this commit is the new way of constructing a text shaper
using text.ShaperOptions. Providing no options will result in a shaper that uses solely
system fonts. The various options can be used to disable system font loading and to
provide an already-parsed collection of fonts as per Gio's old API.
The material.NewTheme function now accepts no arguments instead of a font collection.
Users wanting to provide a collection can simply provide a new shaper configured how
they would like:
theme := material.NewTheme()
theme.Shaper = text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Regular()))
This commit touches many packages to fix up their construction of text shapers, mostly in
test code. The changes to the tests in package widget deserve special note:
Changing our font resolution logic caused the tofu characters within the
test strings to use a different font's tofu. This isn't a problem, but shifted
the layout of the shaped text a little bit. I've updated the numbers to expect
the new glyph positions.
Fixes: https://todo.sr.ht/~eliasnaur/gio/309
Fixes: https://todo.sr.ht/~eliasnaur/gio/184
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
This commit defines an environment-variable-based debug mechanism allowing
users to toggle various debug features of their applications at runtime. The
only currently supported features are debug logging in the text stack and
suppressing the usage message that would otherwise be printed if you supplied
a malformed GIODEBUG value. The syntax is a comma-delimited list of features
right now. To see the usage, set the variable to the empty string (or any other
unsupported value):
$ GIODEBUG="" go run .
To suppress the usage message, use GIODEBUG=silent. This may be helpful for scripts
trying to activate debug features and inspect their output across versions of Gio
with different debug options available.
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
This commit alters the android backend to automatically populate some environment
variables as early as possible in application startup. Specifically, this commit
sets the XDG_{CONFIG,CACHE}_HOME environment variables which are necessary for
the text shaper to infer a valid cache file location.
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
We only support the most recent two go versions, and using 1.18 prevents use of
atomic.Bool, failing CI for a different patchset of mine.
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
This commit ensures that any given paragraph of text shaped by Gio will use a single
internal line height. This line height is determined (by default) by the text size,
rather than the fonts involved. This is a breaking change, as previously we would
blindly use the largest line height of any font in a line for that line, leading to
lines within the same paragraph with extremely uneven spacing. This commit also
updates some test expectations in package widget.
I thought pretty hard about how to implement line spacing, and consulted a few sources:
[0] https://www.figma.com/blog/line-height-changes/
[1] https://practicaltypography.com/line-spacing.html
[2] https://developer.mozilla.org/en-US/docs/Web/CSS/line-height
There is no single, universal way to think about line spacing. Fonts internally specify
a line height as the sum of their ascent, descent, and gap, but the line height of two
fonts at the same pixel size (say 20 Sp) can vary wildy (especially across writing systems).
There are two strategies we could pursue to establish the line height of a paragraph of text:
- derive the line height from the fonts involved (our old behavior, and the behavior of
many word processors)
- derive the line height from the requested text size provided by the user (the behavior of the
web).
The challenge with the first option is that for a given piece of text in the UI, there can
be a silly number of fonts involved. If a label dispays user-generated content, the user can
put an emoji in it, and emoji fonts have different line heights from latin ones. This can cause
unexpected and nasty layout shift. Gio would previously do exactly this, on a line-by-line basis,
resulting in unevenly spaced lines within a paragraph depending on which fonts were used on
which lines. Choosing one of the fonts and enforcing its line height would make things consistent,
but it isn't clear how to choose that canonical font. There is no 1:1 mapping between the input
text.Font provided in the shaping parameters and a single font.Face. Instead, that mapping depends
upon the runes being shaped.
I think the only sane way to implement the first option would be to synthesize some text in the
provided system.Locale (mapping the language to a script and then generating a rune from that
script), shape that single rune, and then enforce the line height of the resulting face on the
entire paragraph. This would require doing a fair bit more work per paragraph than Gio does today,
so I've opted not to do it.
Instead, the second option allows us to choose a line height based on the size of the text that
the user wants to display. While this can potentially interact poorly with unusually tall fonts,
it means that text will always have a consistent line height.
I've provided two knobs to control line height:
- text.Parameters.LineHeight lets you set a specific height in pixels with a default value of
text.Parameters.PxPerEm.
- text.Parameters.LineHeightScale applies a scaling factor to the LineHeight, allowing you to
easily space out text without hard-coding a specific pixel size. The default value here
(drawn from the recommendations of [1]) is 1.2, which looks pretty good across many fonts.
I've chosen this two-value API because many users will want to set one or the other value. I
considered instead a single value field and a "mode" that would specify how it was used, but
that felt uglier. Also, you *can* set both of these two fields and get predictable results.
I'd like to revisit using the line height of the chosen fonts in the future, but it seems a
little too complex to be worthwhile right now. An interesting option would be making the
select-a-face-using-locale strategy described above an opt-in feature, though some users
might instead want to just use the tallest line height among fonts in use. Something like
this Android API might be appropriate:
[3] https://learn.microsoft.com/en-us/dotnet/api/android.widget.textview.fallbacklinespacing?view=xamarin-android-sdk-13
I'd like to thank Dominik Honnef for some good discussion around this feature, and for pointing
me to some good sources on the subject.
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
In the general case, it isn't possible for us to efficiently find system fonts that
are monospace. Fonts don't advertise being monospace frequently, so the only way to
reliably detect it is to check that all glyphs are the same width. This is expensive,
far too much so to be done on every system font when there may be thousands of them.
Other font resolution systems rely upon the user requesting fonts by their family name.
If you want a monospace font, ask for it by name or use a generic name like 'monospace'.
This will be Gio's approach from here on out.
Existing code relying upon setting Variant="Mono" should instead set Typeface="Go Mono"
(for the Go font) or specify another monospace typeface. The generic face "monospace"
will search for one of a set of known monospace fonts that may be available on the system.
Similarly, smallcaps isn't well advertised and users should rely on requesting all-smallcaps
fonts by typeface. To get the Go smallcaps font, use Typeface="Go Smallcaps".
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
This commit fixes a bug that would incorrectly baseline bitmap glyphs text if the line
contained another font with a taller line height. The logic for computing the y offset of
the glyph incorrectly assumed that the Glyph.Ascent was particular to the glyph instead of
the line. I've updated it to use a glyph-specific value.
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
This commit updates the logic that computes scroll viewport coordinates to correctly
consume layout.Position.OffsetLast, which was previously ignored. The impact of ignoring
that field was that dragging on a scroll indicator could sometimes fail to reach the
end of the list.
I've updated the logic to consume that field, which increased the amount of visual
jitter in the position of the scrollbar. I then also added a mechanism for smoothing
the jitter by using both methods of deriving the viewport and synthesizing a viewport
from both.
This new strategy exhibits a lower standard deviation than the other options on each of:
- the length of the scroll indicator
- the change in the start coordinate of the viewport when scrolling smoothly
- the change in the end coordinate of the viewport when scrolling smoothly
Fixes: https://todo.sr.ht/~eliasnaur/gio/504
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
When building GPU vertices from paths, we call stroke.SplitCubic once
per OpCubic. Before this change, each call to stroke.SplitCubic would
allocate a slice, which we would only use to iterate over.
This allocation can be easily avoided by reusing the slice. We can
conveniently store it in gpu.quadSplitter.
In a real application that renders hundreds of paths with tens of
rounded rectangles per path, this saved roughly 4500 allocations (or 1
MB worth) per frame.
Signed-off-by: Dominik Honnef <dominik@honnef.co>
There are many times when an application wants to know metadata about shaped text without
allocating a stateful text widget such as widget.Selectable. This commit introduces widget.TextInfo
and adds an extra LayoutDetailed method to widget.Label returning this struct. Currently
the struct only provides the information necessary to determine whether the text was truncated
(useful for deciding whether a tooltip makes sense), but it can be expanded to include text metrics
in the future for applications which require those.
In the future other text widgets may surface methods of acquiring widget.TextInfos, but the critical
gap in the API is that we can't currently determine whether a stateless label was truncated, so
I'm starting here.
I considered making Label.Layout() always return this, but I didn't want to introduce a breaking
API change yet. I have some other thoughts I want to explore about the label API which might
trigger breaking changes (moving parameters into fields).
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
Now that all events are not emitted at the top level, there is no longer
a way to receive the clipboard event generated by this window-global
clipboard read method. As such, this commit drops the useless and confusing
method from the exported API.
Fixes: https://todo.sr.ht/~eliasnaur/gio/501
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
This commit enables consumers of the text shaper to select a policy for how
line breaking candidates will be chosen. The new default policy can break lines
within "words" (UAX#14 segments) when words do not fit by themselves on a line.
This ensures that text does not horizontally overflow its bounding box unless
the available width is insufficient to display a single UAX#29 grapheme cluster.
Fixes: https://todo.sr.ht/~eliasnaur/gio/467
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
When consuming text from an io.Reader, the shaper could hit an EOF when reading the
text, then still try to check whether it was done by calling ReadByte() followed by
UnreadByte(). The ReadByte() would still return EOF, but the UnreadByte() would then
walk the iterator cursor backwards to the final byte of the text. If and only if the
text was being truncated, this unexpected cursor position could cause the shaper to
conclude that there were additional runes that were truncated, and thus the returned
glyph stream would account for too many runes. This commit provides a test and a fix.
Many thanks to Jack Mordaunt for the excellent bug report leading to this fix.
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
There doesn't seem to be a need for a two-step shutdown sequence, so a
single channel is enough to trigger destruction of the Window.
References: https://todo.sr.ht/~eliasnaur/gio/497
Signed-off-by: Elias Naur <mail@eliasnaur.com>
This commit adds two helper methods to layout.Contraints that make it easier to
manipulate the constraints while keeping their invariants. In particular, code
manually manipulating constraints usually fails to correctly ensure that the
max does not become smaller than the min, the min does not exceed the max, and
that no value goes below zero.
It's quite a few lines to check these invariants yourself in every custom layout,
so I think it makes sense to offer helpers for this.
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
This commit fixes a problem in the unpacking of text.GlyphID on 32 bit architectures.
Incorrectly casting to an `int` on those platforms resulted in truncating the faceIndex
to always be zero. To catch mistakes like this in the future, I've added tests for this
problem that should be run by our new 32-bit CI testing.
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
This commit introduces a 32-bit test run to our Linux CI in an attempt
to detect architecture dependent bugs earlier. I was forced to install
the i386 packages in a build step becuase they can only be added after
enabling the architecture. Also GOARCH=386 does not support the race
detector, so I'm not running the tests with race detection enabled for
that GOARCH.
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
Some devices with high refresh rates limit SurfaceView apps to 60hz
and need a specific API call to set it back. Same approach is used by
https://github.com/ajinasokan/flutter_displaymode. The extra work is
skipped on the devices that don't need it.
Signed-off-by: Ilia Demianenko <ilia.demianenko@gmail.com>
The previous docs claimed that failing to set a textMaterial would result in
invisible glyphs when in reality it results in using whatever the current paint
material is. This could be the paint material from before laying out the glyphs,
or it could be the material for a bitmap glyph. As such, it's better to say that
the color is undefined.
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
This commit removes some inefficiencies from the pre-shaper-cache processing of
text. The text is no longer decoded into runes prior to being tested against the
cache, and the search for newlines uses slightly more efficient iteration operations
now.
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
This commit switches to the new Regular() collection method in gofont,
ensuring that the regular face is only ever loaded once.
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
This commit introduces a special mechanism to load only the regular version
of the Go font. This is useful for Gio to load a font for drawing window
decorations without forcing applications to load every Go font.
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
This commit updates the internal representation of a font to separate the
threadsafe and non-threadsafe operations in a way that enures font.Faces can
be shared by all text shapers in an application. This should ensure that applications
only need to parse fonts a single time, saving a great deal of memory for
applications that open many windows (which each need a different text shaper).
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
Egon pointed out that the current default is unusable on touch screens in Slack, so this
change should hopefully ensure the indicator is interactable on touch devices.
I considered expanding the minor axis dimensions as well, but I don't know what value to
use. The 38DP default would be enormous on non-mobile displays if we made that the default
width.
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
This commit adds back support for loading font collections, which we
lost when switching to the harfbuzz-based shaper last January. In
addition, this commit takes advantage of our new font loading library's
metadata facilities to automatically construct text.FontFaces for all
fonts within a collection. This is significantly more ergonomic for
users, and can be used to load single fonts with automatic metadata
detection as well.
I've exposed a opentype.Face.Font() method that can be used to get the
font metadata for a given face as well, though you have to type assert to
see it:
var myFace text.Face
if asOpentype, ok := myFace.(opentype.Face); ok {
myFont := asOpentype.Font()
}
The one problem with this approach is that the font variant field always
be automatically populated. Mono font detection is supported, but
other variants like SmallCaps are more complicated and may need to be
expressed differently in the future (smallcaps is a feature that any font
file can have, not necessarily a separate font file). See this [0] upstream
issue for details.
Additionally, in order to avoid import cycles, I've moved the declarations
of font attributes to package font. You can fix your code automatically to
refer to the new definitions by running the following:
gofmt -w -r 'text.FontFace -> font.FontFace' .
gofmt -w -r 'text.Variant -> font.Variant' .
gofmt -w -r 'text.Style -> font.Style' .
gofmt -w -r 'text.Typeface -> font.Typeface' .
gofmt -w -r 'text.Font -> font.Font' .
gofmt -w -r 'text.Regular -> font.Regular' .
gofmt -w -r 'text.Italic -> font.Italic' .
gofmt -w -r 'text.Thin -> font.Thin' .
gofmt -w -r 'text.ExtraLight -> font.ExtraLight' .
gofmt -w -r 'text.Light -> font.Light' .
gofmt -w -r 'text.Normal -> font.Normal' .
gofmt -w -r 'text.Medium -> font.Medium' .
gofmt -w -r 'text.SemiBold -> font.SemiBold' .
gofmt -w -r 'text.Bold -> font.Bold' .
gofmt -w -r 'text.ExtraBold -> font.ExtraBold' .
gofmt -w -r 'text.Black -> font.Black' .
gofmt -w -r 'text.Hairline -> font.Thin' .
gofmt -w -r 'text.UltraLight -> font.ExtraLight' .
gofmt -w -r 'text.DemiBold -> font.SemiBold' .
gofmt -w -r 'text.UltraBold -> font.ExtraBold' .
gofmt -w -r 'text.Heavy -> font.Black' .
gofmt -w -r 'text.ExtraBlack -> font.Black+50' .
gofmt -w -r 'text.UltraBlack -> font.ExtraBlack' .
Make sure each affected file imports gioui.org/font.
[0] https://github.com/go-text/typesetting/issues/57
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
Putting a string in an interface value has to (normally) heap allocate
the string header and string. However, putting the address of a local
string variable in an interface value has the same effect, as this
causes the local variable to escape to the heap.
Signed-off-by: Dominik Honnef <dominik@honnef.co>
This commit picks up improvements in upstream go-text that (among other things)
allow the shaper to reuse a lot of information when shaping the same font face
multiple times (using an LRU cache to keep that information available). I've
tried to pick a reasonable default LRU size of 32 faces.
My simple benchmarks indicate a definitive performance gain and reduction in
memory use across the board, which is especially noticable for complex fonts
like arabic and emoji.
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
This commit combs through the logic of computing glyph sizes and positions,
attempting to remove all unnecessary rounding and truncation. This is in
an effort to help text display consistently when different-length strings
are displayed near one another.
The specific problem prompting this change was end-aligned text stacked in
rows with a common suffix. If the rows displayed different values, they
would shape such that those final glyphs were at different fractional x
coordinates, and then they would be aligned with rounding that could display
them at different x positions in spite of the fact that both suffixes are
the same glyphs.
By removing rounding from Alignment.Align, the largest problem is fixed, but
I'm also removing other unnecessary loss of precision that can circumstantially
contribute to this sort of visual issue.
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
Clicking on the window border or the title bar initiates resizing and
moving of the window respectively. This commit fixes a bug where this
would cause a stuck pressed primary button, as we won't receive a
release event. The fix is to only update the set of pressed buttons
after we've decided not to invoke window management.
This fixes a regression introduced by
2957d007a2.
Signed-off-by: Dominik Honnef <dominik@honnef.co>
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.
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
This change adds ViewEvent for JS/WASM, which returns the HTMLElement
which Gio is been rendered, once started.
Signed-off-by: inkeliz <inkeliz@inkeliz.com>
This commit fixes a subtle problem when trunating text widgets that contain
multiple newline-delimited paragraphs.
Paragraphs are the unit of text shaping, so we divide the text into paragraphs
and then iterate those paragraphs performing shaping and line wrapping. If we
have a maximum number of lines to fill, we stop iterating paragraphs when we
use all of the available lines. Usually, if we fill all of the lines the text
shaper will insert the truncator symbol. However, if we exactly fill all of the
lines with the end of a paragraph, the line wrapper is able to fill the line
quota without actually truncating any of the text in that paragraph. Thus it
doesn't insert a truncator even though subsequent paragraphs were truncated (it
has no way to know).
To fix this, I've taught the line wrapper about an explicit scenario in which
we always want to show the truncator symbol *if* we hit the line limit, even if
all of the text in the current paragraph fit. I've then plumbed support for
that through our text stack.
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
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.
Callers of text.(*Shaper).LayoutString should change:
shaper.LayoutString(params, minWidth, maxWidth, locale, "string")
to
params.MinWidth=minWidth
params.MaxWidth=maxWidth
params.Locale=locale
shaper.LayoutString(params, "string")
Callers of text.(*Shaper).Layout should do likewise.
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
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.
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
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.
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
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.
When specifying a maximum number of lines, a truncator symbol is always
used. If the user does not provide one, the rune `…` is used. This
requirement results in a better user experience and significantly simpler
code, as we can rely upon the presence of one or more truncator glyphs in
the output glyph stream when truncation has occurred.
When interacting with truncated text, the truncator glyphs all act as
a single, indivisible unit. They can be selected or not, and if selected
they act as the entire contents of the truncated portion of the text.
This means that copying all of a truncated label will copy the entire
label text content, with the truncator symbol not appearing at all.
Concretely, the exposed text API now accepts a Truncator string in
text.Parameters, and there is a new glyph flag FlagTruncator which indicates
that the glyph is part of the truncator run. The truncator run will only
have a single FlagClusterBreak (even if the run would usually have many),
and the glyph with both FlagClusterBreak and FlagTruncator will have the
quantity of truncated runes in its Runes field. This necessitated increasing
the size of the Runes field from a byte to an int, as it's theoretically possible
for quite a lot of text to be truncated.
This commit necessarily bumps our go-text/typesetting dependency to the version
exposing truncation in the exported API.
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
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.
I chose to implement this within widgets for two reasons:
- grapheme cluster boundaries would be extremely difficult to encode within the
glyph stream returned by the text shaper
- not all text needs to be segmented, only text that can be interacted with
All mutation operations exposed by widget.Editor now work in terms of grapheme
clusters instead of runes.
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
This commit allows the glyph index type to be reset and reused, preventing the
reallocation of numerous buffers when indexing glyphs.
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>