Commit Graph

360 Commits

Author SHA1 Message Date
Chris Waldon b09ef80d9f 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.

Fixes: https://todo.sr.ht/~eliasnaur/gio/487
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-03-23 16:57:52 -06:00
Serhat Sevki Dincer 35a8231963 text,widget: remove ineffective assignments
Signed-off-by: Serhat Sevki Dincer <jfcgauss@gmail.com>
2023-03-23 16:37:46 -06:00
Dominik Honnef 5f818bc5e7 widget/material: use more efficient way of scrolling lists
Signed-off-by: Dominik Honnef <dominik@honnef.co>
2023-02-23 18:43:50 -06:00
Gordon Klaus db6b4de0f7 widget/material: [API] move widget.Float.{Axis,Invert} into material.SliderStyle
Signed-off-by: Gordon Klaus <gordon.klaus@gmail.com>
2023-01-27 21:04:32 -06:00
Gordon Klaus 22aa00f476 widget/material: add Float.Invert
Setting Float.Invert=true not only inverts the order of values (which was already easily done by swapping min and max), it also draws the widget inverted so that the track is darkened on the opposite side from usual.

This patch also fixes a bug wherein a vertical slider was drawn inverted by default.

Signed-off-by: Gordon Klaus <gordon.klaus@gmail.com>
2023-01-27 21:04:01 -06:00
Larry Clapp e0cf570339 widget: add a Focus() method to widget.Clickable
Signed-off-by: Larry Clapp <larry@theclapp.org>
2023-01-18 16:28:18 -06:00
Chris Waldon 1eb5c7dbcd widget: use caller-provided buffers for reading out text
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>
2023-01-14 09:54:06 -06:00
Chris Waldon 940f0f6021 widget: document MoveWord current limitations
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-01-14 09:53:22 -06:00
Chris Waldon 1c49532447 widget: update ByteOffset method docs
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-01-14 09:53:09 -06:00
Chris Waldon 044390c9df widget: document update and paint methods on textView
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>
2023-01-14 09:52:55 -06:00
Chris Waldon f6d56dba89 widget: drop obsolete comment fragment
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-01-14 09:52:51 -06:00
Egon Elbre c81a1f9671 widget: fix build for go1.17
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>
2023-01-06 18:26:10 -06:00
Chris Waldon aa2a948b86 text,widget: [API] drop runereader based shaping API
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>
2022-12-23 09:31:52 -06:00
Chris Waldon dc6fbf07f0 widget: expose text region resolution
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>
2022-12-23 09:31:48 -06:00
Chris Waldon e98c8955bb widget{,/material}: rebuild label and editor with textView
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>
2022-12-23 09:31:45 -06:00
Chris Waldon f99aff96ee widget: create standalone textView
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>
2022-12-23 09:31:40 -06:00
Chris Waldon 0b456579a9 widget: add ReadOnly mode to editor
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>
2022-12-20 11:08:02 -06:00
Chris Waldon c455f0f342 text,widget: test and fix minWidth alignment
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>
2022-12-19 11:17:16 -06:00
Chris Waldon fe5878bc63 widget: track minWidth of editor for alignment
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>
2022-12-19 11:17:12 -06:00
Elias Naur 5d1d1df206 text,widget: use != for flag tests
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-12-16 17:32:30 -06:00
Chris Waldon 12da71821a widget: update glyph iteration
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>
2022-12-16 17:27:16 -06:00
Chris Waldon 719278bb36 widget: unify text painting and fix premature termination
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>
2022-12-14 09:00:31 -06:00
Chris Waldon b7d126e24c font/{gofont,opentype},text,widget{,/material}: [API] add font fallback and bidi support
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>
2022-12-13 22:06:57 -06:00
Chris Waldon 5c84cf7e90 widget: do not allow invalid utf8 in editor
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>
2022-11-16 16:22:43 -06:00
Chris Waldon 4f5a6b3212 widget: define text rendering benchmarks
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>
2022-11-09 08:45:51 -06:00
Elias Naur c67d8cde4b widget: implement triple click line selection in Editor
Fixes: https://todo.sr.ht/~eliasnaur/gio/455
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-11-07 07:30:04 -06:00
Elias Naur 5c896eabbb gesture,widget: detect multi-click on pointer.Press
References: https://todo.sr.ht/~eliasnaur/gio/455
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-11-07 07:26:19 -06:00
Chris Waldon 9f62230c38 widget: adjust editor tests to new pos iteration
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>
2022-10-22 18:19:56 -06:00
Chris Waldon f7c14e9964 widget: redefine >= and ++ on combinedPos
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>
2022-10-22 18:19:51 -06:00
Chris Waldon 2340664570 widget: test and document seekPosition
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>
2022-10-22 18:19:46 -06:00
Chris Waldon dee3cc44f9 widget: test positionGreaterOrEqual
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>
2022-10-22 18:19:41 -06:00
Chris Waldon 64db3720fc widget: test and document clusterIndexFor
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>
2022-10-22 18:19:35 -06:00
Chris Waldon b67b322978 widget: define incrementing combinedPos and test
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>
2022-10-22 18:19:19 -06:00
Chris Waldon 1be58a2bc4 widget: test firstPos
This commit adds a test to lock in the correct behavior of the
firstPos helper method.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2022-10-22 18:19:15 -06:00
Chris Waldon b46c0f5907 widget: use reliable text direction checks, not heuristics
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>
2022-10-22 18:19:08 -06:00
Elias Naur 24eb1a4fc5 widget: make the InputOp key.Set empty for unfocused Editors
Fixes: https://todo.sr.ht/~eliasnaur/gio/448
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-09-24 08:48:30 -06:00
Chris Waldon 020eb27ff5 widget: add useful state accessors to scrollbar
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>
2022-08-29 15:11:51 +02:00
Elias Naur a55065af9c widget: take deleted runes into account when applying Editor.MaxLen
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-08-08 16:15:21 +02:00
Elias Naur 5fc9312f46 widget: report SubmitEvents for IME newlines in submit mode
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>
2022-08-08 15:43:36 +02:00
Elias Naur 61b2e37691 all: format comments with go fmt ./... using Go 1.19
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-08-06 12:26:03 +02:00
Elias Naur f7bc744a24 widget: add Editor.Filter for filtering unwanted characters
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-07-26 15:22:27 +02:00
Elias Naur 26e71011f5 widget: don't let unfocused Clickables swallow key presses
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-07-25 10:57:22 +02:00
Chris Waldon 41de0048db widget: implement editor undo/redo
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>
2022-07-17 14:45:59 +02:00
Elias Naur 1d9ab65313 widget: add Editor.MaxLen for limiting the content length og Editor
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-07-13 18:45:02 +02:00
Elias Naur 48e9cdaffd widget: emit only one ChangeEvent per Editor.Layout
ChangeEvent contains no information, so emitting multiple instances
per layout is pointless.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-07-13 18:13:59 +02:00
Elias Naur 53da73de35 widget: ensure that Border.Layout dimensions fully contains the border
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-07-10 14:59:50 +02:00
Egon Elbre b4acc239cd widget: fix Enum with Return
The input op started listening to Return, however the check
was looking for Enter.

Signed-off-by: Egon Elbre <egonelbre@gmail.com>
2022-07-07 09:36:43 +02:00
Dominik Honnef 992f568ac7 widget: when clicking on scrollbar, center on that point
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>
2022-06-30 08:01:49 +02:00
Dominik Honnef 6981a88720 widget: move scrollbar indicator if dragging starts outside of it
Signed-off-by: Dominik Honnef <dominik@honnef.co>
2022-06-30 08:01:49 +02:00
Dominik Honnef f229601e2d widget: consider size of indicator when limiting scrollbar dragging
Signed-off-by: Dominik Honnef <dominik@honnef.co>
2022-06-30 08:01:49 +02:00