~whereswaldon/gio-x

87be19b6aeab427d5b16d7a2eb93294f822171ff — Chris Waldon a month ago bc95442
component: add TipArea for displaying tooltips

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
1 files changed, 101 insertions(+), 0 deletions(-)

M component/tooltip.go
M component/tooltip.go => component/tooltip.go +101 -0
@@ 1,10 1,14 @@
package component

import (
	"image"
	"image/color"
	"time"

	"gioui.org/f32"
	"gioui.org/io/pointer"
	"gioui.org/layout"
	"gioui.org/op"
	"gioui.org/op/clip"
	"gioui.org/op/paint"
	"gioui.org/unit"


@@ 82,3 86,100 @@ func (t Tooltip) Layout(gtx C) D {
		}),
	)
}

// TipArea holds the state information for displaying a tooltip.
type TipArea struct {
	VisibilityAnimation
	Appeared     time.Time
	HoverStarted time.Time
	Hovering     bool
	PressStarted time.Time
	Pressing     bool
	LongPressed  bool
	init         bool
}

const (
	tipAreaHoverDelay        = time.Millisecond * 500
	tipAreaLongPressDuration = time.Millisecond * 1500
	tipAreaFadeDuration      = time.Millisecond * 250
	longPressTheshold        = time.Millisecond * 500
)

// Layout renders the provided widget with the provided tooltip. The tooltip
// will be summoned if the widget is hovered or long-pressed.
func (t *TipArea) Layout(gtx C, tip Tooltip, w layout.Widget) D {
	if !t.init {
		t.init = true
		t.VisibilityAnimation.State = Invisible
		t.VisibilityAnimation.Duration = tipAreaFadeDuration
	}
	for _, e := range gtx.Events(t) {
		e, ok := e.(pointer.Event)
		if !ok {
			continue
		}
		switch e.Type {
		case pointer.Enter:
			t.Hovering = true
			t.HoverStarted = gtx.Now
		case pointer.Leave:
			t.VisibilityAnimation.Disappear(gtx.Now)
			t.Hovering = false
		case pointer.Press:
			t.Pressing = true
			t.PressStarted = gtx.Now
		case pointer.Release:
			t.Pressing = false
		case pointer.Cancel:
			t.Pressing = false
			t.Hovering = false
		}
	}
	if t.Hovering || t.Pressing || t.LongPressed {
		op.InvalidateOp{}.Add(gtx.Ops)
	}
	if t.Hovering && gtx.Now.Sub(t.HoverStarted) > tipAreaHoverDelay {
		t.VisibilityAnimation.Appear(gtx.Now)
		t.Appeared = gtx.Now
	}
	if t.Pressing && gtx.Now.Sub(t.PressStarted) > longPressTheshold {
		t.LongPressed = true
		t.VisibilityAnimation.Appear(gtx.Now)
		t.Appeared = gtx.Now
	}
	if t.LongPressed && gtx.Now.Sub(t.Appeared) > tipAreaLongPressDuration {
		t.VisibilityAnimation.Disappear(gtx.Now)
		t.LongPressed = false
	}
	return layout.Stack{}.Layout(gtx,
		layout.Stacked(w),
		layout.Expanded(func(gtx C) D {
			defer op.Save(gtx.Ops).Load()
			pointer.PassOp{Pass: true}.Add(gtx.Ops)
			pointer.Rect(image.Rectangle{Max: gtx.Constraints.Min}).Add(gtx.Ops)
			pointer.InputOp{
				Tag:   t,
				Types: pointer.Press | pointer.Release | pointer.Enter | pointer.Leave,
			}.Add(gtx.Ops)

			originalMin := gtx.Constraints.Min
			gtx.Constraints.Min = image.Point{}

			if t.Visible() {
				macro := op.Record(gtx.Ops)
				tip.Bg = Interpolate(color.NRGBA{}, tip.Bg, t.VisibilityAnimation.Revealed(gtx))
				dims := tip.Layout(gtx)
				call := macro.Stop()
				xOffset := float32((originalMin.X / 2) - (dims.Size.X / 2))
				yOffset := float32(originalMin.Y)
				macro = op.Record(gtx.Ops)
				op.Offset(f32.Pt(xOffset, yOffset)).Add(gtx.Ops)
				call.Add(gtx.Ops)
				call = macro.Stop()
				op.Defer(gtx.Ops, call)
			}
			return D{}
		}),
	)
}