-
Notifications
You must be signed in to change notification settings - Fork 830
feat(tui): reduce animations #1250
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 3 commits
6e43d81
a6c3bf4
ba02a4b
cbfd5e6
175acf2
6553c3f
e4dd17e
0c4dd5c
0213bca
53941c9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -28,7 +28,7 @@ const ( | |
| // | ||
| // If the FPS is 20 (50 milliseconds) this means that the ellipsis will | ||
| // change every 8 frames (400 milliseconds). | ||
| ellipsisAnimSpeed = 8 | ||
| ellipsisanimSpeed = 8 | ||
|
||
|
|
||
| // The maximum amount of time that can pass before a character appears. | ||
| // This is used to create a staggered entrance effect. | ||
|
|
@@ -94,13 +94,18 @@ type Settings struct { | |
| GradColorA color.Color | ||
| GradColorB color.Color | ||
| CycleColors bool | ||
| Static bool | ||
| } | ||
|
|
||
| // Default settings. | ||
| const () | ||
| // Spinner is a Bubble for a spinner. | ||
| type Spinner interface { | ||
| Init() tea.Cmd | ||
| Update(tea.Msg) (Spinner, tea.Cmd) | ||
| View() string | ||
| SetLabel(string) | ||
| } | ||
|
|
||
| // Anim is a Bubble for an animated spinner. | ||
| type Anim struct { | ||
| type anim struct { | ||
| width int | ||
| cyclingCharWidth int | ||
| label *csync.Slice[string] | ||
|
|
@@ -117,9 +122,16 @@ type Anim struct { | |
| id int | ||
| } | ||
|
|
||
| // New creates a new Anim instance with the specified width and label. | ||
| func New(opts Settings) *Anim { | ||
| a := &Anim{} | ||
| // New creates a new anim instance with the specified width and label. | ||
| func New(opts Settings) Spinner { | ||
| if colorIsUnset(opts.LabelColor) { | ||
| opts.LabelColor = defaultLabelColor | ||
| } | ||
|
|
||
| if opts.Static { | ||
| return newStatic(opts.Label, opts.LabelColor) | ||
| } | ||
|
|
||
| // Validate settings. | ||
| if opts.Size < 1 { | ||
| opts.Size = defaultNumCyclingChars | ||
|
|
@@ -130,10 +142,8 @@ func New(opts Settings) *Anim { | |
| if colorIsUnset(opts.GradColorB) { | ||
| opts.GradColorB = defaultGradColorB | ||
| } | ||
| if colorIsUnset(opts.LabelColor) { | ||
| opts.LabelColor = defaultLabelColor | ||
| } | ||
|
|
||
| a := &anim{} | ||
| a.id = nextID() | ||
| a.startTime = time.Now() | ||
| a.cyclingCharWidth = opts.Size | ||
|
|
@@ -254,7 +264,7 @@ func New(opts Settings) *Anim { | |
| } | ||
|
|
||
| // SetLabel updates the label text and re-renders it. | ||
| func (a *Anim) SetLabel(newLabel string) { | ||
| func (a *anim) SetLabel(newLabel string) { | ||
| a.labelWidth = lipgloss.Width(newLabel) | ||
|
|
||
| // Update total width | ||
|
|
@@ -268,7 +278,7 @@ func (a *Anim) SetLabel(newLabel string) { | |
| } | ||
|
|
||
| // renderLabel renders the label with the current label color. | ||
| func (a *Anim) renderLabel(label string) { | ||
| func (a *anim) renderLabel(label string) { | ||
| if a.labelWidth > 0 { | ||
| // Pre-render the label. | ||
| labelRunes := []rune(label) | ||
|
|
@@ -295,7 +305,7 @@ func (a *Anim) renderLabel(label string) { | |
| } | ||
|
|
||
| // Width returns the total width of the animation. | ||
| func (a *Anim) Width() (w int) { | ||
| func (a *anim) Width() (w int) { | ||
| w = a.width | ||
| if a.labelWidth > 0 { | ||
| w += labelGapWidth + a.labelWidth | ||
|
|
@@ -313,12 +323,10 @@ func (a *Anim) Width() (w int) { | |
| } | ||
|
|
||
| // Init starts the animation. | ||
| func (a *Anim) Init() tea.Cmd { | ||
| return a.Step() | ||
| } | ||
| func (a *anim) Init() tea.Cmd { return stepCmd(a.id) } | ||
|
|
||
| // Update processes animation steps (or not). | ||
| func (a *Anim) Update(msg tea.Msg) (tea.Model, tea.Cmd) { | ||
| func (a *anim) Update(msg tea.Msg) (Spinner, tea.Cmd) { | ||
| switch msg := msg.(type) { | ||
| case StepMsg: | ||
| if msg.id != a.id { | ||
|
|
@@ -334,20 +342,20 @@ func (a *Anim) Update(msg tea.Msg) (tea.Model, tea.Cmd) { | |
| if a.initialized.Load() && a.labelWidth > 0 { | ||
| // Manage the ellipsis animation. | ||
| ellipsisStep := a.ellipsisStep.Add(1) | ||
| if int(ellipsisStep) >= ellipsisAnimSpeed*len(ellipsisFrames) { | ||
| if int(ellipsisStep) >= ellipsisanimSpeed*len(ellipsisFrames) { | ||
| a.ellipsisStep.Store(0) | ||
| } | ||
| } else if !a.initialized.Load() && time.Since(a.startTime) >= maxBirthOffset { | ||
| a.initialized.Store(true) | ||
| } | ||
| return a, a.Step() | ||
| return a, stepCmd(a.id) | ||
| default: | ||
| return a, nil | ||
| } | ||
| } | ||
|
|
||
| // View renders the current state of the animation. | ||
| func (a *Anim) View() string { | ||
| func (a *anim) View() string { | ||
| var b strings.Builder | ||
| step := int(a.step.Load()) | ||
| for i := range a.width { | ||
|
|
@@ -372,18 +380,18 @@ func (a *Anim) View() string { | |
| // have been initialized. | ||
| if a.initialized.Load() && a.labelWidth > 0 { | ||
| ellipsisStep := int(a.ellipsisStep.Load()) | ||
| if ellipsisFrame, ok := a.ellipsisFrames.Get(ellipsisStep / ellipsisAnimSpeed); ok { | ||
| if ellipsisFrame, ok := a.ellipsisFrames.Get(ellipsisStep / ellipsisanimSpeed); ok { | ||
| b.WriteString(ellipsisFrame) | ||
| } | ||
| } | ||
|
|
||
| return b.String() | ||
| } | ||
|
|
||
| // Step is a command that triggers the next step in the animation. | ||
| func (a *Anim) Step() tea.Cmd { | ||
| // stepCmd is a command that triggers the next stepCmd in the animation. | ||
| func stepCmd(id int) tea.Cmd { | ||
| return tea.Tick(time.Second/time.Duration(fps), func(t time.Time) tea.Msg { | ||
| return StepMsg{id: a.id} | ||
| return StepMsg{id: id} | ||
| }) | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| package anim | ||
|
|
||
| import ( | ||
| "cmp" | ||
| "image/color" | ||
|
|
||
| tea "github.com/charmbracelet/bubbletea/v2" | ||
| "github.com/charmbracelet/lipgloss/v2" | ||
| ) | ||
|
|
||
| type noAnim struct { | ||
| Color color.Color | ||
| rendered string | ||
| id int | ||
| } | ||
|
|
||
| func newStatic(label string, foreground color.Color) Spinner { | ||
| a := &noAnim{Color: foreground} | ||
| a.SetLabel(label) | ||
| a.id = nextID() | ||
| return a | ||
| } | ||
|
|
||
| func (s *noAnim) SetLabel(label string) { | ||
| s.rendered = lipgloss.NewStyle(). | ||
| Foreground(s.Color). | ||
| Render(cmp.Or(label, "Working") + ellipsisFrames[2]) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think showing just a white This is probably a job for @meowgorithm 🙂
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed—I can come up with some stuff. |
||
| } | ||
|
|
||
| func (s noAnim) Init() tea.Cmd { return stepCmd(s.id) } | ||
| func (s *noAnim) View() string { return s.rendered } | ||
| func (s *noAnim) Update(msg tea.Msg) (Spinner, tea.Cmd) { | ||
| switch msg := msg.(type) { | ||
| case StepMsg: | ||
| if msg.id != s.id { | ||
| // Reject messages that are not for this instance. | ||
| return s, nil | ||
| } | ||
| return s, stepCmd(s.id) | ||
| default: | ||
| return s, nil | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should eventually consider documenting these on the README. All of them are missing currently.
Maybe this is a good time to do it.