When storing a string in an interface value that escapes, Go has to heap
allocate space for the string header, as interface values can only store
pointers. In text-heavy applications, this can lead to hundreds of
allocations per frame due to semantic.LabelOp, the primary user of
string-typed references in ops.
Instead of allocating each string header individually, provide a slice
of strings to store string-typed references in, and store pointers into
this slice as the actual references. This only allocates when resizing
the slice's backing array, and averages out to no allocations, as the
backing array gets reused between calls to Ops.Reset.
We introduce two new functions, Write1String and Write2String, which
make use of this new slice for their last argument. We could've
automated this in the existing Write1 and Write2 methods, but that would
require type assertions on each call, and the vast majority of ops do
not make use of strings.
Signed-off-by: Dominik Honnef <dominik@honnef.co>
NSView only has events for left, right, and other. Also, the Go side
wasn't actually checking for buttons other than left and right.
Signed-off-by: Dominik Honnef <dominik@honnef.co>
This commit updates the logic behind SemanticAt to use the same hit area
traversal as normal event routing, which should result in more accurate
results for screen readers trying to resolve widgets that might be partially
obscured by non-semantic content.
While here, I realized that the iteration of hit areas needed to stop at
the first matching semantic area, and I added that capability and updated
the ActionAt logic to leverage it as well.
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
Without eglTerminate, using EGL will crash or report spurious errors after
creating and destroying enough contexts. The test program in #528
takes 5-10 window cycles before errors show up for me.
Fixes: https://todo.sr.ht/~eliasnaur/gio/528
Signed-off-by: Elias Naur <mail@eliasnaur.com>
This commit changes the shaper's behavior when truncating text. Previously, if the final
line allowed by MaxLines ended with a newline, whether or not that newline was truncated
depended upon whether we knew that there was more text after the current paragraph. However,
this makes reasoning about what the shaper will do quite difficult. It seems better to be
consistent. Now we will insert a truncator at the end of the final line if it has a trailing
newline character, regardless of whether it ends the input text.
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
This commit reverts the work of several previous attempts to resolve truncation-related
rune accounting problems and adopts a simpler approach. Instead of taking a special codepath
when shaping only a newline, we shape the empty string to get its line metrics. Instead of
modifying the final glyph conditionally to account for runes we never actually shaped, we
track that count on the document type and handle it withing the NextGlyph method.
These changes result in much simpler code, and resolve a real bug. We were accidentally corrupting
cached paragraphs when doing the truncation post-processing in Shaper.layoutText. The modification
made to the final glyph there actually did modify the cached copy, which would then be reused when
that string was shaped again (even if there were a different number of truncated runes after it).
This changeset ensures that the cached copy of a paragraph is never modified.
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
Commit c0c25b777 replaced the synchronizing of the display link callback
from a sync.Map to a cgo.Handle. However, the change didn't take into
account the lifecycle issues: a callback may happen just as the cgo.Handle
is freed, leading to a misuse crash.
This change restores the sync.Map synchronization, which avoids the
lifecycle issue.
Fixes: https://todo.sr.ht/~eliasnaur/gio/526
Signed-off-by: Elias Naur <mail@eliasnaur.com>
This commit fixes a platform inconsistency that prevented custom-decorated windows
from being resizable on edges where their custom decorations placed a draggable
system.ActionInputOp.
The prior behavior always checked for this action type before
checking if the cursor was potentially in a window resize area, which meant that
for windows with material.Decorations, it was impossible to resize those windows
from their top edge. The system.ActionMove handler would always win. This is not
the case on platforms like macOS, so this commit makes the behavior consistent by
prioritizing resize over drag.
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
This commit fixes the non-intuitive behaviour, where hitting return or
space with a button focused, then tabbing to another button and
releasing the key causes the second button to trigger. It feels wrong,
as the "gesture" was never initiated on the second button. The fix makes
widget.Clickable track which key was pressed, in a variable called
pressedKey, and only considers a key release if the released key matches
the pressed key. Finally, if the widget loses focus, pressedKey is
cleared.
Fixes: https://todo.sr.ht/~eliasnaur/gio/525
Signed-off-by: Veikko Sariola <5684185+vsariola@users.noreply.github.com>
When running ActionAt, the router used to only consider the topmost clip area, even
if that clip area had no input handlers attached whatsoever. This change updates the
logic for that test to use the same traversal as normal event handling, ensuring that
action inputs behave intuitively like any other pointer input area. Included is a test
catching the problematic behavior that prompted this change.
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
NewShaper cannot be called prior to opening an application window on Android unless
the application does not want system font support. Add a note to this effect to the
constructor.
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
This commit ensures that multiple newlines in a row still produce expected
results when occuring within a truncated string. The problem was that we usually
wrap text that is truncated in a way that forces the truncator symbol to appear
at the end *unless* we know we're on the final paragraph of the input text. This
is the right behavior for text that will be displayed, but when shaping a paragraph
containing nothing but a newline, we do not want the truncator symbol in our line.
I simply had to disable the forced truncation contextually to make it work.
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
This commit updates us to a version of go-text that correctly provides text
dimensions for the empty string when laying it out with width zero. Previously,
zero width would result in text with no height.
Fixes: https://todo.sr.ht/~eliasnaur/gio/518
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
This commit fixes another rune accounting bug that the fuzzer discovered. If we
shaped a space in order to acquire line metrics, but the space itself was truncated,
we would reset the truncated count to zero. This had the side effect of lying to
later logic about whether the truncator run was present at the end of the shaped
text.
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
This commit updates the shaper fuzzer to try truncating the text, exposing
new edge cases.
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
This commit fixes another rune accounting issue that only existed when shaping
a solitary newline with zero width while truncating the line.
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
This commit tests and fixes some edge cases that threw off rune accounting
when a newline character was the final rune in the input *and* the text was
being truncated. I imagine they were never previously reported because it's
rare to try to truncate such text.
Thanks to Dominik Honnef for the report and reproducer.
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
We previously were not handling glyphs that extended vertically beyond the
ascent/descent declared by their font. This is done rarely with text fonts,
but is apparently common among symbol and emoji fonts.
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
This commit removes the logic that calculates the bounding box of a line.
We don't actually use this information anywhere, so computing it is just
a waste of CPU and memory. Widgets arrive at their own bounding boxes from
consuming the glyph stream anyway.
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
This commit updates the strategy of our cursor positioning index to eliminate
cursor positions *after* trailing whitespace characters on a line. Eliminating
such cursor positions enables us to collapse trailing whitespace visually without
impacting the editability of text (this will be done in a future commit).
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
The previous logic kept the y offset of a line as a fractional value
until the last possible moment in an effort to be as true to a fractional
line height as possible (minimize the error), but this interacts pathologically
with multi-line text selections, as the selections may have visibly different
gaps between lines. It's better to always shift lines by a fixed quantity of
whole pixels, even if it is technically less accurate to the desired line height.
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
This commit updates our version of go-text to pick up important bugfixes to the
line wrapper (fixing some fuzzer-discovered bugs).
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
This commit ensures that we properly handle the case in which an input string is only
a newline character. We now make a run of text by shaping a space rune and then
drop the glyph/rune data from the space (keeping the line height and such).
The prior behavior would shape zero runes, resulting in no output runs, and thus our
logic for synthesizing a glyph for the newline would never execute while iterating the
runs.
I tried to restructure to instead catch whether there were zero runs after the iteration,
but it came out much uglier and harder to understand that way.
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
This commit surfaces fields to manipulate the line height of all label and editor
types. It's unfortunate how this spreads through the API, but I don't see a good
way to eliminate that right now.
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
This commit introduces the material.Theme.Face field, which will automatically
populate the Font.Typeface in every text widget created using a constructor function
in package material.
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
This commit adds a parser for a simple domain-specific language that
can express a comma-delimited list of font families within a string.
I chose to encode families in this way because the string can be used
as an efficient hash key in a way that a slice of families cannot. Similarly,
using a slice of families would require allocations on the caller side.
The particular format was chosen to allow lists to be written with as little
fanfare as possible. This is why quotation marks are completely optional. It's
easy to read:
Times New Roman, Georgia, serif
Why force the user to type this (this will parse the same):
"Times New Roman", "Georgia", "serif"
I've tried to handle edge cases exhaustively. Commas are legal within quotes.
Within a quoted string, you can escape instances of the surrounding quote with
a backslash, and can escape literal backslashes by adding another backslash.
I wrote the lexer/parser by hand, and I hope that they're both easy to understand
and (if need be) extend.
A side effect of the DSL I've chosen (and part of my reasoning for allowing both
single and double quoted strings) is that CSS font-family rules will generally be
valid font family lists in Gio. This means the syntax is already familiar to users
coming from other technologies, and that you can copy from a web-based application
to get a similar font stack in Gio.
Fixes: https://todo.sr.ht/~eliasnaur/gio/317
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
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>