fix(tracker/gioui): make own TipArea ensuring tips don't stay around

Closes #141.
This commit is contained in:
5684185+vsariola@users.noreply.github.com
2025-06-23 18:02:05 +03:00
parent 31007515b5
commit 6f1db6b392
5 changed files with 156 additions and 12 deletions

View File

@ -17,7 +17,6 @@ import (
"gioui.org/text"
"gioui.org/unit"
"gioui.org/widget"
"gioui.org/x/component"
"github.com/vsariola/sointu/tracker"
)
@ -27,7 +26,7 @@ type (
history []widget.Press
requestClicks int
TipArea component.TipArea // since almost all buttons have tooltips, we include the state for a tooltip here for convenience
TipArea TipArea // since almost all buttons have tooltips, we include the state for a tooltip here for convenience
}
ButtonStyle struct {
@ -289,13 +288,6 @@ func (b *ToggleIconButton) Layout(gtx C) D {
return b.IconButton.Layout(gtx)
}
func Tooltip(th *Theme, tip string) component.Tooltip {
tooltip := component.PlatformTooltip(&th.Material, tip)
tooltip.Bg = th.Tooltip.Bg
tooltip.Text.Color = th.Tooltip.Color
return tooltip
}
// Click executes a simple programmatic click.
func (b *Clickable) Click() {
b.requestClicks++

View File

@ -14,7 +14,6 @@ import (
"gioui.org/op/paint"
"gioui.org/unit"
"gioui.org/widget"
"gioui.org/x/component"
"gioui.org/gesture"
"gioui.org/io/event"
@ -31,7 +30,7 @@ type (
dragStartXY float32
clickDecrease gesture.Click
clickIncrease gesture.Click
tipArea component.TipArea
tipArea TipArea
}
NumericUpDownStyle struct {

150
tracker/gioui/tooltip.go Normal file
View File

@ -0,0 +1,150 @@
package gioui
import (
"image"
"image/color"
"time"
"gioui.org/io/event"
"gioui.org/io/pointer"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/op/clip"
"gioui.org/x/component"
)
// TipArea holds the state information for displaying a tooltip. The zero
// value will choose sensible defaults for all fields.
type TipArea struct {
component.VisibilityAnimation
Hover component.InvalidateDeadline
Press component.InvalidateDeadline
LongPress component.InvalidateDeadline
Exit component.InvalidateDeadline
init bool
// HoverDelay is the delay between the cursor entering the tip area
// and the tooltip appearing.
HoverDelay time.Duration
// LongPressDelay is the required duration of a press in the area for
// it to count as a long press.
LongPressDelay time.Duration
// LongPressDuration is the amount of time the tooltip should be displayed
// after being triggered by a long press.
LongPressDuration time.Duration
// FadeDuration is the amount of time it takes the tooltip to fade in
// and out.
FadeDuration time.Duration
// ExitDuration is the amount of time the tooltip will remain visible at
// maximum, to avoid tooltips staying visible indefinitely if the user
// managed to leave the area without triggering a pointer.Leave event.
ExitDuration time.Duration
}
const (
tipAreaHoverDelay = time.Millisecond * 500
tipAreaLongPressDuration = time.Millisecond * 1500
tipAreaFadeDuration = time.Millisecond * 250
longPressTheshold = time.Millisecond * 500
tipAreaExitDelay = time.Millisecond * 5000
)
// 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 component.Tooltip, w layout.Widget) D {
if !t.init {
t.init = true
t.VisibilityAnimation.State = component.Invisible
if t.HoverDelay == time.Duration(0) {
t.HoverDelay = tipAreaHoverDelay
}
if t.LongPressDelay == time.Duration(0) {
t.LongPressDelay = longPressTheshold
}
if t.LongPressDuration == time.Duration(0) {
t.LongPressDuration = tipAreaLongPressDuration
}
if t.FadeDuration == time.Duration(0) {
t.FadeDuration = tipAreaFadeDuration
}
if t.ExitDuration == time.Duration(0) {
t.ExitDuration = tipAreaExitDelay
}
t.VisibilityAnimation.Duration = t.FadeDuration
}
for {
ev, ok := gtx.Event(pointer.Filter{
Target: t,
Kinds: pointer.Press | pointer.Release | pointer.Enter | pointer.Leave,
})
if !ok {
break
}
e, ok := ev.(pointer.Event)
if !ok {
continue
}
switch e.Kind {
case pointer.Enter:
t.Hover.SetTarget(gtx.Now.Add(t.HoverDelay))
t.Exit.SetTarget(gtx.Now.Add(t.ExitDuration))
case pointer.Leave:
t.VisibilityAnimation.Disappear(gtx.Now)
t.Hover.ClearTarget()
t.Exit.ClearTarget()
case pointer.Press:
t.Press.SetTarget(gtx.Now.Add(t.LongPressDelay))
case pointer.Release:
t.Press.ClearTarget()
case pointer.Cancel:
t.Hover.ClearTarget()
t.Press.ClearTarget()
t.Exit.ClearTarget()
}
}
if t.Hover.Process(gtx) {
t.VisibilityAnimation.Appear(gtx.Now)
}
if t.Press.Process(gtx) {
t.VisibilityAnimation.Appear(gtx.Now)
t.LongPress.SetTarget(gtx.Now.Add(t.LongPressDuration))
}
if t.LongPress.Process(gtx) {
t.VisibilityAnimation.Disappear(gtx.Now)
}
if t.Exit.Process(gtx) {
t.VisibilityAnimation.Disappear(gtx.Now)
}
return layout.Stack{}.Layout(gtx,
layout.Stacked(w),
layout.Expanded(func(gtx C) D {
defer pointer.PassOp{}.Push(gtx.Ops).Pop()
defer clip.Rect(image.Rectangle{Max: gtx.Constraints.Min}).Push(gtx.Ops).Pop()
event.Op(gtx.Ops, t)
originalMin := gtx.Constraints.Min
gtx.Constraints.Min = image.Point{}
if t.Visible() {
macro := op.Record(gtx.Ops)
tip.Bg = component.Interpolate(color.NRGBA{}, tip.Bg, t.VisibilityAnimation.Revealed(gtx))
dims := tip.Layout(gtx)
call := macro.Stop()
xOffset := (originalMin.X / 2) - (dims.Size.X / 2)
yOffset := originalMin.Y
macro = op.Record(gtx.Ops)
op.Offset(image.Pt(xOffset, yOffset)).Add(gtx.Ops)
call.Add(gtx.Ops)
call = macro.Stop()
op.Defer(gtx.Ops, call)
}
return D{}
}),
)
}
func Tooltip(th *Theme, tip string) component.Tooltip {
tooltip := component.PlatformTooltip(&th.Material, tip)
tooltip.Bg = th.Tooltip.Bg
tooltip.Text.Color = th.Tooltip.Color
return tooltip
}

View File

@ -228,7 +228,7 @@ type ParameterWidget struct {
unitBtn Clickable
unitMenu Menu
Parameter tracker.Parameter
tipArea component.TipArea
tipArea TipArea
}
type ParameterStyle struct {