~eliasnaur/gio

9d0a53fc9f8e9c8d8cd72b8482a450667d5705a9 — Chris Waldon 6 months ago 5e6e121
widget{,/material}: [API] split interactive and non-interactive text widgets

This commit separates the types for interactive and non-interactive text within
package widget. widget.Selectable is used for all interactive text. widget.Label
is used for all non-interactive text. There is no longer a field on widget.Label
to provide it with a Selectable. If you want selectable text and are not relying
upon the material pacakge API, you need to create widget.Selectables instead of
widget.Labels. The material package's LabelStyle API is unchanged.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
M widget/label.go => widget/label.go +4 -21
@@ 16,7 16,8 @@ import (
	"golang.org/x/image/math/fixed"
)

// Label is a widget for laying out and drawing text.
// Label is a widget for laying out and drawing text. Labels are always
// non-interactive text. They cannot be selected or copied.
type Label struct {
	// Alignment specifies the text alignment.
	Alignment text.Alignment


@@ 25,28 26,10 @@ type Label struct {
	// Truncator is the text that will be shown at the end of the final
	// line if MaxLines is exceeded. Defaults to "…" if empty.
	Truncator string
	// Selectable optionally provides text selection state. If nil,
	// text will not be selectable.
	Selectable *Selectable
}

// Layout the label with the given shaper, font, size, text, and materials. If the Selectable field is
// populated, the label will support text selection. Otherwise, it will be non-interactive. The textMaterial
// and selectionMaterial op.CallOps are responsible for setting the painting material for the text glyphs
// and the text selection rectangles, respectively.
func (l Label) Layout(gtx layout.Context, lt *text.Shaper, font text.Font, size unit.Sp, txt string, textMaterial, selectionMaterial op.CallOp) layout.Dimensions {
	if l.Selectable == nil {
		return l.layout(gtx, lt, font, size, txt, textMaterial, selectionMaterial)
	}
	l.Selectable.text.Alignment = l.Alignment
	l.Selectable.text.MaxLines = l.MaxLines
	l.Selectable.text.Truncator = l.Truncator
	l.Selectable.SetText(txt)
	return l.Selectable.Layout(gtx, lt, font, size, textMaterial, selectionMaterial)
}

// layout the text as non-interactive.
func (l Label) layout(gtx layout.Context, lt *text.Shaper, font text.Font, size unit.Sp, txt string, textMaterial, selectionMaterial op.CallOp) layout.Dimensions {
// Layout the label with the given shaper, font, size, text, and material.
func (l Label) Layout(gtx layout.Context, lt *text.Shaper, font text.Font, size unit.Sp, txt string, textMaterial op.CallOp) layout.Dimensions {
	cs := gtx.Constraints
	textSize := fixed.I(gtx.Sp(size))
	lt.LayoutString(text.Parameters{

M widget/material/button.go => widget/material/button.go +1 -1
@@ 119,7 119,7 @@ func (b ButtonStyle) Layout(gtx layout.Context) layout.Dimensions {
		return b.Inset.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
			colMacro := op.Record(gtx.Ops)
			paint.ColorOp{Color: b.Color}.Add(gtx.Ops)
			return widget.Label{Alignment: text.Middle}.Layout(gtx, b.shaper, b.Font, b.TextSize, b.Text, colMacro.Stop(), op.CallOp{})
			return widget.Label{Alignment: text.Middle}.Layout(gtx, b.shaper, b.Font, b.TextSize, b.Text, colMacro.Stop())
		})
	})
}

M widget/material/checkable.go => widget/material/checkable.go +1 -1
@@ 76,7 76,7 @@ func (c *checkable) layout(gtx layout.Context, checked, hovered bool) layout.Dim
			return layout.UniformInset(2).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
				colMacro := op.Record(gtx.Ops)
				paint.ColorOp{Color: c.Color}.Add(gtx.Ops)
				return widget.Label{}.Layout(gtx, c.shaper, c.Font, c.TextSize, c.Label, colMacro.Stop(), op.CallOp{})
				return widget.Label{}.Layout(gtx, c.shaper, c.Font, c.TextSize, c.Label, colMacro.Stop())
			})
		}),
	)

M widget/material/editor.go => widget/material/editor.go +1 -1
@@ 61,7 61,7 @@ func (e EditorStyle) Layout(gtx layout.Context) layout.Dimensions {

	macro := op.Record(gtx.Ops)
	tl := widget.Label{Alignment: e.Editor.Alignment, MaxLines: maxlines}
	dims := tl.Layout(gtx, e.shaper, e.Font, e.TextSize, e.Hint, hintColor, selectionColor)
	dims := tl.Layout(gtx, e.shaper, e.Font, e.TextSize, e.Hint, hintColor)
	call := macro.Stop()

	if w := dims.Size.X; gtx.Constraints.Min.X < w {

M widget/material/label.go => widget/material/label.go +11 -2
@@ 14,6 14,9 @@ import (
	"gioui.org/widget"
)

// LabelStyle configures the presentation of text. If the State field is set, the
// label will be laid out as interactive (able to be selected and copied). Otherwise,
// the label will be non-interactive.
type LabelStyle struct {
	// Face defines the text style.
	Font text.Font


@@ 109,6 112,12 @@ func (l LabelStyle) Layout(gtx layout.Context) layout.Dimensions {
	paint.ColorOp{Color: l.SelectionColor}.Add(gtx.Ops)
	selectColor := selectColorMacro.Stop()

	tl := widget.Label{Alignment: l.Alignment, MaxLines: l.MaxLines, Selectable: l.State}
	return tl.Layout(gtx, l.shaper, l.Font, l.TextSize, l.Text, textColor, selectColor)
	if l.State != nil {
		if l.State.Text() != l.Text {
			l.State.SetText(l.Text)
		}
		return l.State.Layout(gtx, l.shaper, l.Font, l.TextSize, textColor, selectColor)
	}
	tl := widget.Label{Alignment: l.Alignment, MaxLines: l.MaxLines}
	return tl.Layout(gtx, l.shaper, l.Font, l.TextSize, l.Text, textColor)
}

M widget/selectable_test.go => widget/selectable_test.go +8 -12
@@ 46,9 46,8 @@ func TestSelectableMove(t *testing.T) {
	gtx.Queue = newQueue(key.FocusEvent{Focus: true})
	s := new(Selectable)

	Label{
		Selectable: s,
	}.Layout(gtx, cache, text.Font{}, fontSize, str, op.CallOp{}, op.CallOp{})
	s.SetText(str)
	s.Layout(gtx, cache, text.Font{}, fontSize, op.CallOp{}, op.CallOp{})

	testKey := func(keyName string) {
		// Select 345


@@ 62,9 61,8 @@ func TestSelectableMove(t *testing.T) {

		// Press the key
		gtx.Queue = newQueue(key.Event{State: key.Press, Name: keyName})
		Label{
			Selectable: s,
		}.Layout(gtx, cache, font, fontSize, str, op.CallOp{}, op.CallOp{})
		s.SetText(str)
		s.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})

		if expected, got := "", s.SelectedText(); expected != got {
			t.Errorf("KeyName %s, expected %q, got %q", keyName, expected, got)


@@ 102,12 100,10 @@ func TestSelectableConfigurations(t *testing.T) {
					gtx.Constraints.Min = gtx.Constraints.Max
				}
				s := new(Selectable)
				label := Label{
					Alignment:  alignment,
					Selectable: s,
				}
				interactiveDims := label.Layout(gtx, cache, font, fontSize, sentence, op.CallOp{}, op.CallOp{})
				staticDims := label.Layout(gtx, cache, font, fontSize, sentence, op.CallOp{}, op.CallOp{})
				s.text.Alignment = alignment
				s.SetText(sentence)
				interactiveDims := s.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
				staticDims := Label{Alignment: alignment}.Layout(gtx, cache, font, fontSize, sentence, op.CallOp{})

				if interactiveDims != staticDims {
					t.Errorf("expected consistent dimensions, static returned %#+v, interactive returned %#+v", staticDims, interactiveDims)

M widget/text_bench_test.go => widget/text_bench_test.go +2 -2
@@ 97,7 97,7 @@ func BenchmarkLabelStatic(b *testing.B) {
		l := Label{}
		b.ResetTimer()
		for i := 0; i < b.N; i++ {
			l.Layout(gtx, cache, font, fontSize, runesStr, op.CallOp{}, op.CallOp{})
			l.Layout(gtx, cache, font, fontSize, runesStr, op.CallOp{})
			if render {
				win.Frame(gtx.Ops)
			}


@@ 132,7 132,7 @@ func BenchmarkLabelDynamic(b *testing.B) {
			a := rand.Intn(len(runes))
			b := rand.Intn(len(runes))
			runes[a], runes[b] = runes[b], runes[a]
			l.Layout(gtx, cache, font, fontSize, string(runes), op.CallOp{}, op.CallOp{})
			l.Layout(gtx, cache, font, fontSize, string(runes), op.CallOp{})
			if render {
				win.Frame(gtx.Ops)
			}