Commit Graph

54 Commits

Author SHA1 Message Date
Chris Waldon 593c5fbf4a text: round y offset of trailing newline
This commit tries to ensure that trailing newlines do not introduce more vertical space below
the text than is occupied by a typical text run within the text.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2025-01-28 10:35:52 -05:00
zjzhang adaace864d text: respect line height when layout the last empty line.
Text Shaper set the last empty line height to ascent+decent
of the paragraph break glyph which causes the last visual empty
line to have a smaller line height. This commit tries to fix it
by setting the line height using the line height from the last line.

References: https://todo.sr.ht/~eliasnaur/gio/629
Signed-off-by: zjzhang <zhangzj33@gmail.com>
2025-01-28 10:17:48 -05:00
Chris Waldon b576252963 text: allow disabling space trimming
This commit adds a shaping parameter that disables the trimming of trailing
whitespace from lines. Text editors and similar use-cases want trailing whitspace
glyphs to be selectable, which means they must occupy space.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2024-12-18 13:28:16 -05:00
Larry Clapp 95f63c66b6 text,widget/material: Update doc for Shaper & Theme
Note that you should use different Themes, with different Shapers, for
different top-level windows, and explain why.

Signed-off-by: Larry Clapp <larry@theclapp.org>
2024-10-22 19:05:27 +02:00
Egon Elbre df8a8789a3 text: [API] reduce size of Glyph.Runes to uint16
This shrinks text.Glyph from 72B to 58B.

  LabelStatic/1000runes-RTL-arabic-32   63.62µ ± 0%   62.05µ ± 0%  -2.47% (p=0.002 n=6)

Signed-off-by: Egon Elbre <egonelbre@gmail.com>
2023-11-09 15:18:46 -05:00
Chris Waldon 83202263b9 text: simplify truncation accounting
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>
2023-08-22 16:37:20 -06:00
Chris Waldon 03c21dc1b5 text: add android portability notice to NewShaper
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>
2023-08-12 08:16:28 -06:00
Chris Waldon 80da4d6b02 text: fix EOF detection at newline boundaries
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>
2023-08-02 09:44:41 -04:00
Chris Waldon 43c47f0883 go.*,text,font{,/opentype},app,gpu,widget{,/material}: [API] load system fonts
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>
2023-07-19 10:01:51 +02:00
Chris Waldon 6ea4119a3c text,widget: [API] implement consistent, controllable line height
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>
2023-07-17 22:33:02 +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 4677b72a4c text: fix over-reading on truncated EOF
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>
2023-05-26 16:34:59 -06:00
Chris Waldon 0e5ec18a82 text: fix 32-bit glyph id packing
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>
2023-04-29 10:00:49 -06:00
Chris Waldon bba91263b0 text: optimize shaper paragraph decoding
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>
2023-04-24 20:33:56 -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 6937a5dd1f go.*,text: update go-text to pick up font transformation caching
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>
2023-04-05 11:32:45 -06:00
Chris Waldon d71f170c29 text: truncate multi-paragraph text correctly
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>
2023-03-30 12:04:08 -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 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 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 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 5d6cc2892d text: consume io.Reader in shaper
io.Reader is actually a more efficient interface than io.RuneReader,
as we can pull bytes out and check for cache hits without doing
redundant rune<->string conversions. This isn't implemented yet,
however.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2022-12-23 09:31:36 -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 5b40d3cd47 text: provide start of paragraph glyph marker
This commit adds a new flag to glyphs indicating that they are the
beginning of a new paragraph, as well as adding a guarantee that a
glyph with this flag will always follow a glyph with FlagParagraphBreak,
even if a paragraph break is the last rune in the text. This helps
widgets to find the boundaries and positions of text ending with
newlines reliably.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2022-12-16 17:27:08 -06:00
Chris Waldon b0483975b7 text: drop unused field on line
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2022-12-15 08:41:40 -06:00
Chris Waldon bfb47538aa text: ensure runereader behaves same as string
This commit fixes a subtle discrepancy in the handling of text input
within the shaper. Text provided as an io.RuneReader with a trailing
newline would generate an extra (empty) line of text, whereas the
same input provided as a string would not.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2022-12-15 08:34:58 -06:00
Chris Waldon 2db1a7bfb9 go.*,text: implement shaper-driven line truncation
This commit pushes limiting the maximum number of lines of text into
the shaper implementation. This is more efficient than doing it in
widgets, and also opens the door for future use of the shaper to
insert ellipsis and other truncating characters as appropriate.

I realized that we lost the implementation of limiting the number of
lines of text in my text stack overhaul, so this fixes a regression
from that work.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2022-12-14 11:44:03 -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
Dominik Honnef e21c665e70 text: optimize faceCache.hashGIDs
Use binary.LittleEndian directly instead of going through the
binary.Write indirection. This allows the following optimizations to
occur:

  - We can reuse our own byte slice between iterations
  - We don't have to put g.ID in an interface value
  - h doesn't escape
  - PutUint32 gets inlined

On top of that, the argument to maphash.Hash.Write doesn't escape, so b
doesn't move to the heap.

Signed-off-by: Dominik Honnef <dominik@honnef.co>
2022-06-28 18:33:31 +02:00
Chris Waldon 9576b659d7 text: [API] remove Text and Advances from Layout
These fields are no longer needed with the new text shaper.
Advances is redundant to the glyph information, and Text
should never be used during layout, as you should
traverse the cluster list instead. This commit also removed
the now-unused string field from the path LRU cache key.

References: https://todo.sr.ht/~eliasnaur/gio/146
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2022-03-18 08:04:27 +01:00
Chris Waldon 1e5a3696f5 deps,text,widget,font/opentype: [API] add harfbuzz-powered text shaper
This commit introduces a new text shaping infrastructure
powered by Benoit Kugler's Go source-port of harfbuzz.
This shaper can properly display complex scripts and RTL
text. This commit changes the signature of the text.Shaper
function, which is a breaking API change.

The new functionality is available via opentype.ParseHarfbuzz,
which configures a text.Shaper leveraging the new backend.

References: https://todo.sr.ht/~eliasnaur/gio/146
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2022-03-18 08:01:44 +01:00
Christophe Meessen a34e239c04 text,widget,opentype: change text.Face.Shape to return a clip.PathSpec
With this change, the Shape function returns a clip.PathSpec
instead of a clip.Outline op. It is then possible to create
a clip.Outline or clip.Stroke op to fill the text path or
draw its stroke.

Signed-off-by: Christophe Meessen <meessen@cppm.in2p3.fr>
2021-12-19 13:30:45 +01:00
Elias Naur c1298cd755 font/opentype,text,widget: use clip.Op for text shapes, not a macro
This change avoids a macro wrapping every text shape, and prepares text
shaping for scoped clip operations.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2021-10-07 15:01:17 +02:00
Walter Werner SCHNEIDER 49a7b2e6f4 text/shaper: lookup closest font by weight
Signed-off-by: Walter Werner SCHNEIDER <contact@schnwalter.eu>
2021-09-04 11:21:55 +02:00
Walter Werner SCHNEIDER 1350b495c8 text/shaper: use consistent method receiver name
Signed-off-by: Walter Werner SCHNEIDER <contact@schnwalter.eu>
2021-09-02 08:54:52 +02:00
Walter Werner SCHNEIDER 83d23ab507 all: sort and group imports
Signed-off-by: Walter Werner SCHNEIDER <contact@schnwalter.eu>
2020-12-17 08:55:09 +01:00
Elias Naur aee87baefe text: represent laid out text as strings to facilitate caching of layouts
Commit https://gioui.org/commit/b331407e81456 added text layout and shaping
based on io.Reader and changed Editor to use it. Unfortunately, as ~inkeliz
discovered, caching of shapes were also lost.

~inkeliz suggested fix,

https://lists.sr.ht/~eliasnaur/gio-patches/patches/15059

adds caching of shapes to Editor to regain lost performance.

This change repairs the cache to work on io.Reader API, in hope that the
already complicated Editor won't need additional caching.

Before this change, text layouts were represented as a slice of (rune, advance)
pairs. Unfortunately, this representation doesn't lend itself to caching of
shaping results, so change the representation of a line of text to be a pair
of text and advances:

	package text

	type Layout {
		Text string
		Advances []fixed.Int26_6
	}

The Text field can then be used in a cache key, assuming Advances is
consistent with it.

The end result is that the two shaper variants of text.Shaper is reduced to
just one, and the Len field field of text.Line is no longer needed.

The changed representation adds a bit of extra work to package opentype.
Cleaning that up is left as a future TODO.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2020-11-16 16:02:30 +01:00
Elias Naur 67594636e7 text: offset Weight constants so the zero value is normal text weight
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2020-11-16 14:54:44 +01:00
Elias Naur 6ab43aba3e all: implement staticcheck suggestions
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2020-07-19 10:47:17 +02:00
Elias Naur 913a780d64 text: remove Metrics from Face interface
It's not used in text shaping, so let's not require it.

Note that the concrete opentype package still retains the Metrics
implementation.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2020-06-26 10:44:19 +02:00
Elias Naur 9e3d3b6f58 text,font/gofont: replace text.Collection with slice of FontFaces
A slice of FontFace pairs are simpler, and thread safe in case a client
wants to append or modify the font collection.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2020-06-21 21:44:28 +02:00
Elias Naur 424a728988 text: fix typo
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2020-06-21 21:14:05 +02:00
Elias Naur b07d34354e text,widget/material: make font collections explicit
Before this change, package font implemented a global font registry,
with the usual problems of package global state.

This change deletes the global registry and introduces the text.Collection
type for representing a list of fonts and their faces. Collection exports
Lookup that finds the closest match and its face.

The existing FontRegistry is renamed to Cache to reflect its new limited
functionality: a cache of shapes and measurements on top of a Collection.

Then, material.NewTheme is changed to take a Collection and initialize
a Cache.

Updates gio#19 because multiple windows require a separate (writable) Cache per
window, while (read-only) Collections may be shared.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2020-06-07 16:27:43 +02:00
Elias Naur 4c220f4554 text: simplify text layout and shaping API
First, replace LayoutOptions with an explicit maximum width parameter.  The
single-field option struct doesn't carry its weight, and I don't think we'll
see more global layout options in the future. Rather, I expect options to cover
spans of text or be part of a Font.

Second, replace the unit.Converter with an scaled text size. It's simpler and
allow the Editor and similar widgets to easily detect whether their cached
layouts are stale. Package text no longer depends on package unit, which is
now dealt with at the widget-level only.

Finally, remove the Size field from Font. It was a design mistake: a Font is
assumed to cover all sizes, as evidenced by the FontRegistry disregarding
Size when looking up fonts.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2020-02-03 23:32:55 +01:00
Elias Naur b331407e81 text: add io.Reader Layout method to Shaper
use them for Editor, which is no longer required to construct a string
for laying out its content.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2020-01-13 21:38:54 +01:00
Elias Naur 16d2a3ac0a text: remove String, Layout and add Glyph
In preparation for using Shaper with an io.Reader, rework the API to not refer
to strings. In particular, introduce Glyph for holding the rune in addition to
the advance. For fast traversing of the underlying text, add Len to Line with
the UTF8 length.

Layout is a useless wrapper around []Line; remove it while we're
here.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2020-01-13 19:54:11 +01:00
Elias Naur e25b1639b9 text: make Shaper an interface
And rename out the caching implementation to FontRegistry.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2020-01-13 14:48:31 +01:00
Elias Naur 3f6a1c34d3 text: correct Shaper comment about the default face
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2020-01-13 13:52:33 +01:00
Werner Laurensse e76726d4f7 text/shaper: Add Metrics method to Shaper struct.
Signed-off-by: Werner Laurensse <werner@alman.ax>
2019-12-15 20:23:13 +01:00
Elias Naur 0768fbe590 text: convert clip.Ops to op.CallOp
MacroOp is about to lose the ability to run a different operation list
than the one it was recorded on. Text shape caches rely on that property,
and must use the new CallOp operation added for purpose.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2019-12-12 00:45:36 +01:00