From 998790f085614001e3ab782a8580220ce7d767e0 Mon Sep 17 00:00:00 2001 From: Andy Williams Date: Sat, 16 Sep 2023 16:38:35 +0100 Subject: [PATCH] Add a new widget for activity indication --- cmd/fyne_demo/tutorials/data.go | 7 +- cmd/fyne_demo/tutorials/widget.go | 47 ++++++++++ widget/activity.go | 145 ++++++++++++++++++++++++++++++ 3 files changed, 198 insertions(+), 1 deletion(-) create mode 100644 widget/activity.go diff --git a/cmd/fyne_demo/tutorials/data.go b/cmd/fyne_demo/tutorials/data.go index e146883ee3..3aa23a03c8 100644 --- a/cmd/fyne_demo/tutorials/data.go +++ b/cmd/fyne_demo/tutorials/data.go @@ -88,6 +88,11 @@ var ( makeAccordionTab, true, }, + "activity": {"Activity", + "A spinner indicating activity used in buttons etc.", + makeActivityTab, + true, + }, "button": {"Button", "Simple widget for user tap handling.", makeButtonTab, @@ -182,6 +187,6 @@ var ( "": {"welcome", "canvas", "animations", "icons", "widgets", "collections", "containers", "dialogs", "windows", "binding", "advanced"}, "collections": {"list", "table", "tree", "gridwrap"}, "containers": {"apptabs", "border", "box", "center", "doctabs", "grid", "scroll", "split"}, - "widgets": {"accordion", "button", "card", "entry", "form", "input", "progress", "text", "toolbar"}, + "widgets": {"accordion", "activity", "button", "card", "entry", "form", "input", "progress", "text", "toolbar"}, } ) diff --git a/cmd/fyne_demo/tutorials/widget.go b/cmd/fyne_demo/tutorials/widget.go index 078273494b..ac4132354a 100644 --- a/cmd/fyne_demo/tutorials/widget.go +++ b/cmd/fyne_demo/tutorials/widget.go @@ -55,6 +55,53 @@ func makeAccordionTab(_ fyne.Window) fyne.CanvasObject { return ac } +func makeActivityTab(_ fyne.Window) fyne.CanvasObject { + a1 := widget.NewActivity() + a2 := widget.NewActivity() + a3 := widget.NewActivity() + + prop := canvas.NewRectangle(color.Transparent) + prop.SetMinSize(fyne.NewSize(160, 80)) + + var button *widget.Button + start := func() { + button.Disable() + a1.Start() + a1.Show() + a2.Start() + a2.Show() + a3.Start() + a3.Show() + + defer func() { + go func() { + time.Sleep(time.Second * 10) + a1.Stop() + a1.Hide() + a2.Stop() + a2.Hide() + a3.Stop() + a3.Hide() + + button.Enable() + }() + }() + } + + button = widget.NewButton("Animate", start) + start() + + fakeDialog := container.NewStack(canvas.NewRectangle(theme.OverlayBackgroundColor()), + container.NewVBox(widget.NewLabelWithStyle("Dialog, e.g.", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}), + container.NewStack(prop, a3))) + + return container.NewCenter(container.NewGridWithColumns(1, + container.NewCenter(container.NewVBox( + container.NewHBox(widget.NewLabel("Working..."), a1), + container.NewStack(button, a2))), + container.NewCenter(fakeDialog))) +} + func makeButtonTab(_ fyne.Window) fyne.CanvasObject { disabled := widget.NewButton("Disabled", func() {}) disabled.Disable() diff --git a/widget/activity.go b/widget/activity.go new file mode 100644 index 0000000000..5d8efc0f58 --- /dev/null +++ b/widget/activity.go @@ -0,0 +1,145 @@ +package widget + +import ( + "image/color" + "time" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/internal/cache" + "fyne.io/fyne/v2/theme" +) + +var _ fyne.Widget = (*Activity)(nil) + +// Activity is used to indicate that something is happening that should be waited for, +// or is in the background (depending on usage). +type Activity struct { + BaseWidget +} + +// NewActivity returns a widget for indicating activity +func NewActivity() *Activity { + a := &Activity{} + a.ExtendBaseWidget(a) + return a +} + +func (a *Activity) MinSize() fyne.Size { + a.ExtendBaseWidget(a) + + return fyne.NewSquareSize(theme.IconInlineSize()) +} + +// Start the activity indicator animation +func (a *Activity) Start() { + if r, ok := cache.Renderer(a.super()).(*activityRenderer); ok { + r.start() + } +} + +// Stop the activity indicator animation +func (a *Activity) Stop() { + if r, ok := cache.Renderer(a.super()).(*activityRenderer); ok { + r.stop() + } +} + +func (a *Activity) CreateRenderer() fyne.WidgetRenderer { + dots := make([]fyne.CanvasObject, 3) + for i := range dots { + dots[i] = canvas.NewCircle(theme.ForegroundColor()) + } + r := &activityRenderer{dots: dots} + r.anim = &fyne.Animation{ + // Curve: fyne.AnimationEaseOut, + Duration: time.Second * 2, + RepeatCount: fyne.AnimationRepeatForever, + Tick: r.animate} + r.updateColor() + return r +} + +var _ fyne.WidgetRenderer = (*activityRenderer)(nil) + +type activityRenderer struct { + anim *fyne.Animation + dots []fyne.CanvasObject + + bound fyne.Size + maxCol color.NRGBA + maxRad float32 +} + +func (a *activityRenderer) Destroy() { + a.stop() +} + +func (a *activityRenderer) Layout(size fyne.Size) { + a.maxRad = fyne.Min(size.Width, size.Height) / 2 + a.bound = size +} + +func (a *activityRenderer) MinSize() fyne.Size { + return fyne.NewSquareSize(theme.IconInlineSize()) +} + +func (a *activityRenderer) Objects() []fyne.CanvasObject { + return a.dots +} + +func (a *activityRenderer) Refresh() { + a.updateColor() +} + +func (a *activityRenderer) animate(done float32) { + off := done * 2 + if off > 1 { + off = 2 - off + } + + off1 := (done + 0.25) * 2 + if done >= 0.75 { + off1 = (done - 0.75) * 2 + } + if off1 > 1 { + off1 = 2 - off1 + } + + off2 := (done + 0.75) * 2 + if done >= 0.25 { + off2 = (done - 0.25) * 2 + } + if off2 > 1 { + off2 = 2 - off2 + } + + a.scaleDot(a.dots[0].(*canvas.Circle), off) + a.scaleDot(a.dots[1].(*canvas.Circle), off1) + a.scaleDot(a.dots[2].(*canvas.Circle), off2) +} + +func (a *activityRenderer) scaleDot(dot *canvas.Circle, off float32) { + rad := a.maxRad - a.maxRad*off/1.2 + mid := fyne.NewPos(a.bound.Width/2, a.bound.Height/2) + + dot.Move(mid.Subtract(fyne.NewSquareOffsetPos(rad))) + dot.Resize(fyne.NewSquareSize(rad * 2)) + + alpha := uint8(0 + int(float32(a.maxCol.A)*off)) + dot.FillColor = color.NRGBA{R: a.maxCol.R, G: a.maxCol.G, B: a.maxCol.B, A: alpha} + dot.Refresh() +} + +func (a *activityRenderer) start() { + a.anim.Start() +} + +func (a *activityRenderer) stop() { + a.anim.Stop() +} + +func (a *activityRenderer) updateColor() { + rr, gg, bb, aa := theme.ForegroundColor().RGBA() + a.maxCol = color.NRGBA{R: uint8(rr >> 8), G: uint8(gg >> 8), B: uint8(bb >> 8), A: uint8(aa >> 8)} +}