This commit alters the textView API to give callers the option to provide
their own buffers for reading text. This enables some widget usecases to
be zero-allocation if a widget simply needs to examine the contents of the
text without returning it as a string.
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
This commit updates the textView to better describe the expectations
and behaviors of the Update and Paint* methods.
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
go.mod specifies 1.18, due to go.mod behavior and to avoid some issues
with updating the dependencies. However, we can still support older go
version, as long as it compiles with the older version.
Signed-off-by: Egon Elbre <egonelbre@gmail.com>
The io.Reader based API has the potential to be significantly more
efficient, and there are very few users of the runereader API. This
commit simply drops it entirely in favor of the reader API.
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
This commit adds exported methods to both LabelState and Editor
allowing callers to locate the text regions representing a range
of runes. This can be used to build interactive subregions of text,
like (for instance) hyperlinks.
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
This commit rebuilds the editor and label types on the common
foundation provided by textView. This enables labels to have
optional state that makes them selectable, and allows the
two widgets to share the code for managing cursor positions,
displaying selections, and soforth. Labels now have an additional
Layout function which can be invoked if they have a Selectable.
It accepts a layout.Widget used to paint their contents. Stateless
labels should still use the old Layout method.
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
This commit adds a standalone state type for manipulating
and displaying text. It reads text from a minimal interface,
shapes it, tracks valid cursor positions, and provides sizing
and scrolling services to higher-level widgets. My long term
goal with these types is to export them to allow non-core widgets
to build atop them, but I've left them private for now.
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
This commit provides a new ReadOnly boolean on the editor. If set, the
editor functions as a selectable label. User interaction cannot change
the contents of the editor (though application code can still use the
API).
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
This commit unifies and fixes the shaper's handling of the alignment
minimum width. Previously it was only considered when the text was
a single line, but in hindsight that was clearly a mistake. Now the
maximum width of all shaped lines and the minimum width is used to
set the text alignment.
This commit also fixes an index test in package widget that was
relying on the old (incorrect) alignment behavior.
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
This commit extends the editor to keep track of its own minimum constraint
and to provide that value to the text shaper for the purpose of aligning
text. Without this, the shaper does not know how much of the width of the
editor to use for alignment purposes.
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
This commit updates the textIterator and glyphIndex types to consume
new flag information provided on glyphs. These changes allow widget.Label
and widget.Editor to correctly compute text bounding boxes and to
generate valid cursor positions at the end of text.
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
This commit unifies all widget text painting to use a single function
and fixes two bugs that could result in visible glyphs failing to be
painted.
The first bug was that we checked whether a particular glyph's
outline was visible within the viewport and terminated iteration the
first time that we found a glyph that wasn't visible. If the very top
of the next line of text was visible within the viewport, taller glyphs
should be painted since part of them is visible. We would stop as soon
as we got to a short glyph, preventing the rest of the line (and any
tall glyphs it contained) from being painted.
I fixed this first problem by using the ascent/descent of the line containing
a glyph to determine whether it's "visible". While this will conclude that
a small glyph is visible when it may be entirely off-screen, the net result
will be that we will paint the entire line containing the glyph rather than
constructing a special version of the line with only the tall glyphs. This
has better path caching performance, as we don't need a bespoke path for when
the line is partially visible.
The second bug was that when the glyph iterator concluded that the
current glyph was out of the viewport, we would immediately terminate
the loop for painting glyphs without painting any buffered glyphs that
had been determined to be visible.
This second bug was easily fixed by ensuring that we always paint all buffered
glyphs when terminating iteration.
As part of this work, I pulled the (fairly complex) logic of buffering and
painting glyphs into the glyph iterator so that label and editor can share
a single implementation.
I was unable to completely encapsulate the array storing buffered glyphs within
the iterator without it being moved to the heap, so the current glyph iteration
API requires the caller to juggle a slice of glyphs. Hopefully someone in
the future can find a structure that the compiler's escape analysis understands.
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
This commit restructures the entire text shaping stack to enable lines of shaped text to
have non-homogeneous properties like which font face they belong to and which direction
a segment of text is going.
The text package now provides a concrete type text.Shaper which can be used to convert
strings into sequences of renderable text.Glyphs. At a high level, the API is used
like this:
// Prepare some fonts.
var collection []text.FontFace
// Make a shaper with those fonts loaded.
shaper := text.NewShaper(collection)
// Shape a string.
shaper.LayoutString(text.Parameters{
PxPerEm: fixed.I(12),
}, 0, 100, system.Locale{}, "Hello")
// Iterate the glyphs from that string.
for glyph, ok := shaper.NextGlyph(); ok; glyph, ok = shaper.NextGlyph() {
// Convert the glyph data into a path. In real uses, convert batches of glyphs
// rather than single glyphs to reduce the number of individual paths and offsets
// required to display your text.
shape := shaper.Shape([]text.Glyph{glyph})
// Offset the glyph to the position it declares within its fields. This will
// automatically handle correct bidirectional text glyph positioning.
offset := op.Offset(image.Pt(glyph.X.Floor(), int(glyph.Y))).Push(gtx.Ops)
// Create a clip area from the shape of the glyph.
area := clip.Outline{Path: shape}.Push(gtx.Ops)
// Paint whatever the current color is within the glyph's shape.
paint.PaintOp{}.Add(gtx.Ops)
area.Pop()
offset.Pop()
}
This API will transparently handle both font fallback (choosing appropriate fonts
from those loaded when the primary font doesn't contain a required glyph) and
bidirectional text (mixed left-to-right and right-to-left text). Glyphs are
iterated in order of the input runes, not their visual order, but proper use
of the provided offsets will ensure that text always displays correctly.
Thanks to Elias Naur for suggesting this glyph iterator strategy. It let us cut
through a lot of accumulated complexity from trying to match our old text APIs,
meaning that this change actually is a net negative change in lines of code.
This commit consumes the upstream github.com/go-text/typesetting/shaping API
now that my prior work is merged there, removing the need for the font/opentype/internal
package entirely.
As part of my efforts, I fuzzed both the low-level text shaping stack and the
editor widget extensively. I've committed regression tests found that way into
the appropriate testdata files to ensure the fuzzer re-checks them.
Fixes: https://todo.sr.ht/~eliasnaur/gio/425
Fixes: https://todo.sr.ht/~eliasnaur/gio/211
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
This commit replaces invalid UTF8 codepoints with the replacement character
when they are inserted into the editor. This ensures that the editor never
moves the editing gap to an invalid location and reads its contents.
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
This commit adds a series of benchmarks for text rendering. They are intended
to capture the performance of static and continuously changing text within
labels and editors, and will serve as a baseline to compare the post-bidi
text stack against.
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
This commit fixes the expectations of our ligature iteration tests to
match the new behavior of the text position iterator. Now the cursor
can reach the position after the final glyph on a line, if that glyph
is not a newline.
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
This commit redefines incrementing a combinedPos to either move a single
rune forward, *or* transition from EOL->BOL, *or* both. This allows traversal
of lines without a trailing newline character to reach the position after the
final glyph of content.
Additionally, this commit updates positionGreaterOrEqual to explicitly handle
hard newlines via special-case logic, allowing lines without a hard newline to
avoid the newline-based short-circuit logic that would prevent them from iteratively
reaching the combinedPos following the final glyph on the line.
Fixes: https://todo.sr.ht/~eliasnaur/gio/400
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
This commit adds a test for the seekPosition helper, a function which can
be used to move a combinedPos forward through a body of text until it approaches
a position.
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
This commit adds an exhaustive test case for the positionGreaterOrEqual
helper function that our text widgets use to compare locations within
shaped text.
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
This commit adds documentation and tests for the clusterIndexFor helper,
making it easier to understand what it does and how to use it safely.
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
This commit restructures seekPosition from a complex state-manipulating
loop into a simple loop of iteratively applying an increment operation
to the combinedPos. The increment operation itself is now tested, and
much easier to understand.
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
This commit switches the way in which the editor and helper functions check
for RTL text from a heuristic to using the actual text direction.
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
This commit adds methods to widget.Scrollbar that enable consuming
code to check if the scroll indicator is processing a drag gesture
or if the scroll track is currently being hovered. These accessors
enable scrollbar style types to have enough information to hide the
scroll indicator when it isn't needed, whereas currently they cannot
differentiate between a scrollbar indicator that is being dragged
but hasn't moved since the last frame and a scrollbar indicator that
is not being dragged.
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
Before this change, an IME text edit would always have its newlines
replaced with spaces. However, for Editors where Submit is enabled
we want newlines to result in SubmitEvents.
Signed-off-by: Elias Naur <mail@eliasnaur.com>
This commit adds a simple linear-history undo/redo mechanism to
widget.Editor bound to Short-(Shift)-Z as well as tests for this
new feature.
Notes on the implementation:
- using a slice to hold the history does mean that we incur
allocations as the user types, but I hope that the Go slice
growth heuristic means that the number of times we pay this
penalty is very small. We also never shrink the slice in this
implementation, which ensures that undoing work and then making
additional modifications is very efficient, but could be framed
as a memory leak.
- this implementation creates a new history element every time
we call replace(). This means that, on desktop, it's essentially
one per rune of input. Users likely want to be able to undo larger
units of change, so a future improvement could be to coalesce
changes so long as the selection doesn't change between them.
- I think it's possible to store only one of the Apply/Reverse
change contents in the history slice, but it's significantly
more complicated. To implement this, you'd need to add a field
indicating if the modification represented a forward or backward
change, and then rewrite the modification's content as you performed
undo/redo operations.For the time being, I'm not sure it's worth
this complexity.
- Future work could introduce a limit to the number of history
entries stored. If we did this, we should also change the
data structure for storing history. Enforcing such a limit
using a simple slice like this would be extremely inefficient.
Perhaps a ring buffer or a linked list would make more sense?
- Applications will likely want to be able to manipulate undo
history in the future. We may wish to export undo() and redo()
from the editor. Applications will also likely want a mechanism
to save the undo history to disk and restore it (implementing
persistent undo). I'm not sure what the most suitable API for
that is yet, so I decided not to try to tackle it yet.
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
Previously, we'd scroll so the new viewportStart corresponded to the
clicked position. This felt okay if clicking above the current
indicator, but felt jarring when clicking below it. Centering gives a
consistent behavior regardless of the scroll direction.
Signed-off-by: Dominik Honnef <dominik@honnef.co>
Before, we would set s.dragging to false on pointer.Release and then
immediately set it back to true because we were processing the event and
saw that s.dragging was false.
Signed-off-by: Dominik Honnef <dominik@honnef.co>
Once the user begins dragging, the cursor can move outside the clip
area (or even the window on at least X11), leading to events with
positions that are either negative, or larger than the clip area.
Negative values outright break the delta tracking and cause the
scrollbar to misbehave. Positive values "only" break the invariant of
Scrollbar.ScrollDistance that the returned value is in the range [-1, 1].
Signed-off-by: Dominik Honnef <dominik@honnef.co>
The app.Window.Perform(ActionMove) is the wrong abstraction for
initiating a move gesture: Windows needs to know the move gesture
area at pointer move, and macOS needs to know the pointer button
down event that triggers the move gesture. This change replaces
Perform(ActionMove) with a new system.ActionInputOp that marks an
area movable.
Signed-off-by: Elias Naur <mail@eliasnaur.com>
Allowing clients to initiate resize gestures is a waste: macOS
doesn't support them, and the only reason we added them was to
implement client-side decorations for Wayland. Now all desktop
platforms implement resize gestures as needed, and we no longer
need the system.Action actions.
Signed-off-by: Elias Naur <mail@eliasnaur.com>
Style values are ephemeral, and pointer methods can't be called in
the same expression a style value is constructed. Matches other style
types.
Signed-off-by: Elias Naur <mail@eliasnaur.com>