diff --git a/CHANGELOG.md b/CHANGELOG.md index 322f5c0..34cdfbf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,6 +52,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). be computed every draw. ([#176][i176]) ### Fixed +- Tooltips will be hidden after certain amount of time has passed, to ensure + that the tooltips don't stay around ([#141][i141]) - BREAKING CHANGE: always first modulate delay time, then apply notetracking. In a delay unit, modulation adds to the delay time, while note tracking multiplies it with a multiplier dependent on the note. The order of these @@ -311,6 +313,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). [i130]: https://github.com/vsariola/sointu/issues/130 [i136]: https://github.com/vsariola/sointu/issues/136 [i139]: https://github.com/vsariola/sointu/issues/139 +[i141]: https://github.com/vsariola/sointu/issues/141 [i142]: https://github.com/vsariola/sointu/issues/142 [i144]: https://github.com/vsariola/sointu/issues/144 [i145]: https://github.com/vsariola/sointu/issues/145 diff --git a/tracker/gioui/buttons.go b/tracker/gioui/buttons.go index ca2bd10..cc73fe8 100644 --- a/tracker/gioui/buttons.go +++ b/tracker/gioui/buttons.go @@ -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++ diff --git a/tracker/gioui/numericupdown.go b/tracker/gioui/numericupdown.go index 2d2b435..252f1cf 100644 --- a/tracker/gioui/numericupdown.go +++ b/tracker/gioui/numericupdown.go @@ -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 { diff --git a/tracker/gioui/tooltip.go b/tracker/gioui/tooltip.go new file mode 100644 index 0000000..721a40f --- /dev/null +++ b/tracker/gioui/tooltip.go @@ -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 +} diff --git a/tracker/gioui/unit_editor.go b/tracker/gioui/unit_editor.go index fd87e23..0f48e2d 100644 --- a/tracker/gioui/unit_editor.go +++ b/tracker/gioui/unit_editor.go @@ -228,7 +228,7 @@ type ParameterWidget struct { unitBtn Clickable unitMenu Menu Parameter tracker.Parameter - tipArea component.TipArea + tipArea TipArea } type ParameterStyle struct {