Commit Graph

17 Commits

Author SHA1 Message Date
Chris Waldon ddf770b9d5 widget{,/material}: surface line height manipulation
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>
2023-07-19 10:05:33 +02:00
Chris Waldon c6e4eecf21 go.*,text,widget{,/material}: enable configurable line wrapping within words
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>
2023-06-07 16:41:14 -06:00
Chris Waldon f77bf9a42c font/opentype: [API] support font collection loading
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>
2023-04-18 16:22:48 -06:00
Chris Waldon 7e8c10927b 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.

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>
2023-03-28 09:25:35 -06:00
Chris Waldon 5e6e1217da 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.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-03-28 09:25:31 -06:00
Chris Waldon 959f5889a1 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.

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>
2023-03-28 09:25:28 -06:00
Chris Waldon 5c54268d40 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.

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>
2023-03-28 09:25:25 -06:00
Chris Waldon 36e768e716 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.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-03-28 09:25:24 -06:00
Chris Waldon 6ab3ff40a6 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.

The consequences of supporting colored glyphs forced changes upon the widget APIs
for widgets that display text. Previously text always had a fixed paint material,
so we could rely upon the caller setting the material (e.g. adding a paint.ColorOp)
before painting the glyphs and everything would work. Now that we display image-
based glyphs, we end up changing the painting material to an image midway through
displaying text. This is an awkward consequence of how we currently manage the
painting material, and to work around it widgets now accept an op.CallOp that
is expected to set the proper paint material. Text widgets will use that op.CallOp
before painting text (or other paint operations) to ensure that they are painting
with the proper materials.

This, in turn, changed the APIs for laying out widget.Editor, widget.Label, and
widget.Selectable, and eliminated the need for them to accept a callback (the
callback was only really to set the colors). Dropping that callback function
allowed me to consolidate widget.Label to only need one exported Layout method,
and allowed me to unexport the PaintText, PaintCaret, and PaintSelection methods
from widget.Editor and widget.Selectable. Those methods are useless in the public
API now that they don't need to be invoked after applying a color operation.

Callers of the raw text shaper API will need to make the following changes:

- Where before you used:

	var ops *op.Ops // Assume we have an operation list.
	var shaper *text.Shaper // Assume we have a shaper.
	var col color.NRGBA // Assume we have a text color.
	var glyphs []text.Glyph // Assume we have already filled a slice of glyphs.

	shape := shaper.Shape(glyphs)
	paint.FillShape(ops, col, clip.Outline{Path:shape}.Op())

- Now you should do:

	shape, call := shaper.Shape(glyphs)
	paint.FillShape(ops, col, clip.Outline{Path:shape}.Op())
	call.Add(ops)

Callers of the widget.{Label,Selectable,Editor} APIs will need to make the
following changes:

- Where before you used:

	var gtx layout.Context // Assume we have an operation list.
	var shaper *text.Shaper // Assume we have a shaper.
	var textCol color.NRGBA // Assume we have a text color.
	var selectCol color.NRGBA // Assume we have a selection color.
	var ed widget.Editor // Assume we have an editor.
	var sel widget.Selectable // Assume we have a selectable.

	// Lay out an editor.
	ed.Layout(gtx, shaper, text.Font{}, unit.Sp(30), func(layout.Context) layout.Dimensions {
		// Paint the editor.
	})
	// Lay out a selectable.
	sel.Layout(gtx, shaper, text.Font{}, unit.Sp(30), func(layout.Context) layout.Dimensions {
		// Paint the selectable.
	})
	// Lay out an interactive label.
	widget.Label{}.LayoutSelectable(gtx, shaper, text.Font{}, unit.Sp(30), "hello", func(layout.Context) layout.Dimensions {
		// Paint the label.
	})
	// Lay out a non-interactive label.
	widget.Label{}.Layout(gtx, shaper, text.Font{}, unit.Sp(30), "hello")

- Now you should do:

	// Capture setting the text paint material in a macro.
	textColMacro := op.Record(gtx.Ops)
	paint.ColorOp{Color: textCol}.Add(gtx.Ops)
	textMaterial := textColMacro.Stop()
	// Capture setting the selection paint material in a macro.
	selectColMacro := op.Record(gtx.Ops)
	paint.ColorOp{Color: selectCol}.Add(gtx.Ops)
	selectMaterial := selectColMacro.Stop()

	// Lay out an editor.
	ed.Layout(gtx, shaper, text.Font{}, unit.Sp(30), textMaterial, selectMaterial)
	// Lay out a selectable.
	sel.Layout(gtx, shaper, text.Font{}, unit.Sp(30), textMaterial, selectMaterial)
	// Lay out a label (no difference between interactive and non-interactive)
	widget.Label{}.Layout(gtx, shaper, text.Font{}, unit.Sp(30), "hello", textMaterial, selectMaterial)

Callers of the material package API do not need to make any changes.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-03-28 09:25:15 -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
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 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