Files
supersonic/ui/toastoverlay.go
T

270 lines
7.0 KiB
Go

package ui
import (
"context"
"time"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/driver/desktop"
"fyne.io/fyne/v2/lang"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
"github.com/dweymouth/supersonic/ui/util"
"github.com/dweymouth/supersonic/ui/widgets"
)
const (
toastAutoDismissalTime = 3 * time.Second
toastAnimationDuration = 100 * time.Millisecond
)
type ToastOverlay struct {
widget.BaseWidget
currentToast *toast
currentToastAnim *fyne.Animation
dismissCancel context.CancelFunc
container *fyne.Container
}
func NewToastOverlay() *ToastOverlay {
t := &ToastOverlay{container: container.NewWithoutLayout()}
t.container.Objects = make([]fyne.CanvasObject, 0, 1)
t.ExtendBaseWidget(t)
return t
}
func (t *ToastOverlay) ShowSuccessToast(message string) {
t.showToast(false, message)
}
func (t *ToastOverlay) ShowErrorToast(message string) {
t.showToast(true, message)
}
func (t *ToastOverlay) showToast(isErr bool, message string) {
t.cancelPreviousToast()
t.currentToast = newToast(isErr, message, t.dismissToast)
t.container.Objects = append(t.container.Objects, t.currentToast)
s := t.Size()
min := t.currentToast.MinSize()
pad := theme.Padding()
t.currentToast.Resize(min)
endPos := fyne.NewPos(s.Width-min.Width-pad, s.Height-min.Height-pad)
startPos := fyne.NewPos(s.Width, endPos.Y)
f := t.makeToastAnimFunc(endPos, false)
t.currentToastAnim = canvas.NewPositionAnimation(startPos, endPos, toastAnimationDuration, f)
t.currentToastAnim.Curve = fyne.AnimationEaseOut
t.currentToastAnim.Start()
t.Refresh()
ctx, cancel := context.WithCancel(context.Background())
t.dismissCancel = cancel // always canceled by dismissToast
go func() {
time.Sleep(toastAutoDismissalTime)
select {
case <-ctx.Done():
return
default:
t.dismissToast()
}
}()
}
func (t *ToastOverlay) makeToastAnimFunc(endPos fyne.Position, dismissal bool) func(fyne.Position) {
return func(p fyne.Position) {
if ct := t.currentToast; ct != nil {
ct.Move(p)
}
if p == endPos {
t.currentToastAnim = nil
if dismissal {
t.cancelPreviousToast()
}
}
}
}
func (t *ToastOverlay) Resize(size fyne.Size) {
if t.currentToast != nil && t.currentToastAnim == nil {
// move to the anchor position
min := t.currentToast.MinSize()
pad := theme.Padding()
t.currentToast.Move(fyne.NewPos(size.Width-min.Width-pad, size.Height-min.Height-pad))
} // else if animation is running -- well, hope this doesn't happen ;)
t.BaseWidget.Resize(size)
}
func (t *ToastOverlay) cancelPreviousToast() {
if t.currentToast == nil {
return
}
if t.currentToastAnim != nil {
t.currentToastAnim.Stop()
t.currentToastAnim = nil
}
t.container.Objects[0] = nil
t.container.Objects = t.container.Objects[:0]
t.currentToast = nil
}
func (t *ToastOverlay) dismissToast() {
if t.currentToast == nil {
return
}
if t.dismissCancel != nil {
t.dismissCancel()
t.dismissCancel = nil
}
if t.currentToastAnim != nil {
t.currentToastAnim.Stop()
}
s := t.Size()
min := t.currentToast.MinSize()
pad := theme.Padding()
startPos := fyne.NewPos(s.Width-min.Width-pad, s.Height-min.Height-pad)
endPos := fyne.NewPos(s.Width, startPos.Y)
f := t.makeToastAnimFunc(endPos, true)
t.currentToastAnim = canvas.NewPositionAnimation(startPos, endPos, toastAnimationDuration, f)
t.currentToastAnim.Curve = fyne.AnimationEaseIn
t.currentToastAnim.Start()
}
func (t *ToastOverlay) CreateRenderer() fyne.WidgetRenderer {
return widget.NewSimpleRenderer(t.container)
}
type toast struct {
widget.BaseWidget
isErr bool
message string
onDismiss func()
}
func newToast(isErr bool, message string, onDismiss func()) *toast {
t := &toast{isErr: isErr, message: message, onDismiss: onDismiss}
t.ExtendBaseWidget(t)
return t
}
func (t *toast) CreateRenderer() fyne.WidgetRenderer {
return newToastRenderer(t)
}
func (t *toast) Dismiss() {
if t.onDismiss != nil {
t.onDismiss()
}
}
// swallow all tap/mouse events because toast is transparent
var (
_ fyne.Tappable = (*toast)(nil)
_ fyne.SecondaryTappable = (*toast)(nil)
_ desktop.Hoverable = (*toast)(nil)
_ desktop.Mouseable = (*toast)(nil)
)
func (*toast) Tapped(*fyne.PointEvent) {}
func (*toast) TappedSecondary(*fyne.PointEvent) {}
func (*toast) MouseIn(*desktop.MouseEvent) {}
func (*toast) MouseOut() {}
func (*toast) MouseMoved(*desktop.MouseEvent) {}
func (*toast) MouseUp(*desktop.MouseEvent) {}
func (*toast) MouseDown(*desktop.MouseEvent) {}
type toastRenderer struct {
container *fyne.Container
background *canvas.Rectangle
accent *canvas.Rectangle
accentColor fyne.ThemeColorName
}
func newToastRenderer(t *toast) *toastRenderer {
title := lang.L("Success")
accentColor := theme.ColorNamePrimary
if t.isErr {
title = lang.L("Error")
accentColor = theme.ColorNameError
}
th := fyne.CurrentApp().Settings().Theme()
v := fyne.CurrentApp().Settings().ThemeVariant()
background := canvas.NewRectangle(th.Color(theme.ColorNameOverlayBackground, v))
background.CornerRadius = th.Size(theme.SizeNameInputRadius)
background.StrokeColor = th.Color(theme.ColorNameInputBorder, v)
background.StrokeWidth = th.Size(theme.SizeNameInputBorder) * 2
accent := canvas.NewRectangle(th.Color(accentColor, v))
accent.SetMinSize(fyne.NewSize(4, 1))
close := widgets.NewIconButton(theme.CancelIcon(), t.Dismiss)
close.IconSize = widgets.IconButtonSizeSmaller
titleText := widget.NewRichTextWithText(title)
titleText.Segments[0].(*widget.TextSegment).Style = widget.RichTextStyleSubHeading
pad := theme.Padding()
return &toastRenderer{
background: background,
accent: accent,
accentColor: accentColor,
container: container.NewStack(
background,
container.New(&layout.CustomPaddedLayout{
TopPadding: 2 * pad,
BottomPadding: 2 * pad,
LeftPadding: 2 * pad,
RightPadding: pad,
},
container.NewBorder(nil, nil, accent, nil,
container.NewVBox(
container.NewBorder(nil, nil, nil, container.NewHBox(close, util.NewHSpace(2)), titleText),
widget.NewLabel(t.message),
),
),
),
),
}
}
var _ fyne.WidgetRenderer = (*toastRenderer)(nil)
func (*toastRenderer) Destroy() {}
func (t *toastRenderer) Layout(s fyne.Size) {
t.container.Layout.Layout(t.container.Objects, s)
}
func (t *toastRenderer) MinSize() fyne.Size {
return t.container.MinSize()
}
func (t *toastRenderer) Objects() []fyne.CanvasObject {
return t.container.Objects
}
func (t *toastRenderer) Refresh() {
th := fyne.CurrentApp().Settings().Theme()
v := fyne.CurrentApp().Settings().ThemeVariant()
t.background.FillColor = th.Color(theme.ColorNameOverlayBackground, v)
t.background.CornerRadius = th.Size(theme.SizeNameInputRadius)
t.background.StrokeColor = th.Color(theme.ColorNameInputBorder, v)
t.background.StrokeWidth = th.Size(theme.SizeNameInputBorder) * 2
t.background.Refresh()
t.accent.FillColor = th.Color(t.accentColor, v)
t.accent.Refresh()
canvas.Refresh(t.container)
}