Text Spinner

Keith Vetter 2019-10-01 : A spinner, also known as a throbber , is an animated graphic element used to show that a computer program is performing an action in the background. It's related to a progress bar except it does not convey how much of the action has been completed. A text spinner is a spinner which just uses characters in its animation. Historically text spinners rotated through the sequence "\" "\" "|" "/", but as this package demonstrates, much fancier ones have been constructed.

This package provides an interface to over 50 different text spinners. It lets you add a spinner to widget or a textvar, and it will automatically update the item at an interval set by the spinner type.

As usual, I've provided a short demo which displays ten different spinners all animating simultaneously.


Jeff Smith 2019-09-02 : Below is an online demo using CloudTk


##+##########################################################################
#
# spinners -- Package that provides 50+ different types of text Spinners.
# by Keith Vetter 2019-10-01
#
# Usage:
#  pack [label .l -textvar my_var]
#    [optional] lassign [::Spinner::Info :random:] spinnerName interval frames maxWidth
#  set id [::Spinner::Start spinnerName my_var]
#  ::Spinner::Stop $id
#
# Can also work without Tk--it will periodically update the variable you give it
#

namespace eval Spinner {
    # Inspired by https://www.npmjs.com/package/cli-spinners
    # and https://stackoverflow.com/questions/2685435/cooler-ascii-spinners
    variable NextID 0
    variable ACTIVE
    variable SPINNERS

    array set SPINNERS {
        dots { 80 { ⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏ }}
        dots2 { 80 { ⣾ ⣽ ⣻ ⢿ ⡿ ⣟ ⣯ ⣷ }}
        dots3 { 80 { ⠋ ⠙ ⠚ ⠞ ⠖ ⠦ ⠴ ⠲ ⠳ ⠓ }}
        dots4 { 80 { ⠄ ⠆ ⠇ ⠋ ⠙ ⠸ ⠰ ⠠ ⠰ ⠸ ⠙ ⠋ ⠇ ⠆ }}
        dots5 { 80 { ⠋ ⠙ ⠚ ⠒ ⠂ ⠂ ⠒ ⠲ ⠴ ⠦ ⠖ ⠒ ⠐ ⠐ ⠒ ⠓ ⠋ }}
        dots6 { 80 { ⠁ ⠉ ⠙ ⠚ ⠒ ⠂ ⠂ ⠒ ⠲ ⠴ ⠤ ⠄ ⠄ ⠤ ⠴ ⠲ ⠒ ⠂ ⠂ ⠒ ⠚ ⠙ ⠉ ⠁ }}
        dots7 { 80 { ⠈ ⠉ ⠋ ⠓ ⠒ ⠐ ⠐ ⠒ ⠖ ⠦ ⠤ ⠠ ⠠ ⠤ ⠦ ⠖ ⠒ ⠐ ⠐ ⠒ ⠓ ⠋ ⠉ ⠈ }}
        dots8 { 80 { ⠁ ⠁ ⠉ ⠙ ⠚ ⠒ ⠂ ⠂ ⠒ ⠲ ⠴ ⠤ ⠄ ⠄ ⠤ ⠠ ⠠ ⠤ ⠦ ⠖ ⠒ ⠐ ⠐ ⠒ ⠓ ⠋ ⠉ ⠈ ⠈ }}
        dots9 { 80 { ⢹ ⢺ ⢼ ⣸ ⣇ ⡧ ⡗ ⡏ }}
        dots10 { 80 { ⢄ ⢂ ⢁ ⡁ ⡈ ⡐ ⡠ }}
        dots11 { 100 { ⠁ ⠂ ⠄ ⡀ ⢀ ⠠ ⠐ ⠈ }}
        dots12 { 80 { ⢀⠀ ⡀⠀ ⠄⠀ ⢂⠀ ⡂⠀ ⠅⠀ ⢃⠀ ⡃⠀ ⠍⠀ ⢋⠀ ⡋⠀ ⠍⠁ ⢋⠁ ⡋⠁ ⠍⠉ ⠋⠉ ⠋⠉ ⠉⠙ ⠉⠙ ⠉⠩ ⠈⢙ ⠈⡙ ⢈⠩ ⡀⢙ ⠄⡙ ⢂⠩ ⡂⢘ ⠅⡘ ⢃⠨ ⡃⢐ ⠍⡐ ⢋⠠ ⡋⢀ ⠍⡁ ⢋⠁ ⡋⠁ ⠍⠉ ⠋⠉ ⠋⠉ ⠉⠙ ⠉⠙ ⠉⠩ ⠈⢙ ⠈⡙ ⠈⠩ ⠀⢙ ⠀⡙ ⠀⠩ ⠀⢘ ⠀⡘ ⠀⠨ ⠀⢐ ⠀⡐ ⠀⠠ ⠀⢀ ⠀⡀ }}
        line { 130 { - \\ | / }}
        line2 { 100 { ⠂ - – — – - }}
        pipe { 100 { ┤ ┘ ┴ └ ├ ┌ ┬ ┐ }}
        simpleDots { 400 { {.  } {.. } ... {   } }}
        simpleDotsScrolling { 200 { {.  } {.. } ... { ..} {  .} {   } }}
        star { 70 { ✶ ✸ ✹ ✺ ✹ ✷ }}
        star2 { 80 { + x * }}
        flip { 70 { _ _ _ - ` ` ' ´ - _ _ _ }}
        hamburger { 100 { ☱ ☲ ☴ }}
        growVertical { 120 { ▁ ▃ ▄ ▅ ▆ ▇ ▆ ▅ ▄ ▃ }}
        growHorizontal { 120 { ▏ ▎ ▍ ▌ ▋ ▊ ▉ ▊ ▋ ▌ ▍ ▎ }}
        balloon { 140 { { } . o O @ * { } }}
        balloon2 { 120 { . o O ° O o . }}
        noise { 100 { ▓ ▒ ░ }}
        bounce { 120 { ⠁ ⠂ ⠄ ⠂ }}
        boxBounce { 120 { ▖ ▘ ▝ ▗ }}
        boxBounce2 { 100 { ▌ ▀ ▐ ▄ }}
        triangle { 50 { ◢ ◣ ◤ ◥ }}
        arc { 100 { ◜ ◠ ◝ ◞ ◡ ◟ }}
        circle { 120 { ◡ ⊙ ◠ }}
        squareCorners { 180 { ◰ ◳ ◲ ◱ }}
        circleQuarters { 120 { ◴ ◷ ◶ ◵ }}
        circleHalves { 50 { ◐ ◓ ◑ ◒ }}
        squish { 100 { ╫ ╪ }}
        toggle { 250 { ⊶ ⊷ }}
        toggle2 { 80 { ▫ ▪ }}
        toggle3 { 120 { □ ■ }}
        toggle4 { 100 { ■ □ ▪ ▫ }}
        toggle5 { 100 { ▮ ▯ }}
        toggle6 { 300 { ဝ ၀ }}
        toggle7 { 80 { ⦾ ⦿ }}
        toggle8 { 100 { ◍ ◌ }}
        toggle9 { 100 { ◉ ◎ }}
        toggle10 { 100 { ㊂ ㊀ ㊁ }}
        toggle11 { 50 { ⧇ ⧆ }}
        toggle12 { 120 { ☗ ☖ }}
        toggle13 { 80 { = * - }}
        arrow { 100 { ← ↖ ↑ ↗ → ↘ ↓ ↙ }}
        arrow2 { 80 { {⬆️ } {↗️ } {➡️ } {↘️ } {⬇️ } {↙️ } {⬅️ } {↖️ } }}
        arrow3 { 120 { ▹▹▹▹▹ ▸▹▹▹▹ ▹▸▹▹▹ ▹▹▸▹▹ ▹▹▹▸▹ ▹▹▹▹▸ }}
        bouncingBar { 80 { {[    ]} {[=   ]} {[==  ]} {[=== ]} {[ ===]} {[  ==]} {[   =]} {[    ]} {[   =]} {[  ==]} {[ ===]} {[====]} {[=== ]} {[==  ]} {[=   ]} }}
        bouncingBall { 80 { {( ●    )} {(  ●   )} {(   ●  )} {(    ● )} {(     ●)} {(    ● )} {(   ●  )} {(  ●   )} {( ●    )} {(●     )} }}
        pong { 80 { {▐⠂       ▌} {▐⠈       ▌} {▐ ⠂      ▌} {▐ ⠠      ▌} {▐  ⡀     ▌} {▐  ⠠     ▌} {▐   ⠂    ▌} {▐   ⠈    ▌} {▐    ⠂   ▌} {▐    ⠠   ▌} {▐     ⡀  ▌} {▐     ⠠  ▌} {▐      ⠂ ▌} {▐      ⠈ ▌} {▐       ⠂▌} {▐       ⠠▌} {▐       ⡀▌} {▐      ⠠ ▌} {▐      ⠂ ▌} {▐     ⠈  ▌} {▐     ⠂  ▌} {▐    ⠠   ▌} {▐    ⡀   ▌} {▐   ⠠    ▌} {▐   ⠂    ▌} {▐  ⠈     ▌} {▐  ⠂     ▌} {▐ ⠠      ▌} {▐ ⡀      ▌} {▐⠠       ▌} }}
        shark { 120 { {▐|\____________▌} {▐_|\___________▌} {▐__|\__________▌} {▐___|\_________▌} {▐____|\________▌} {▐_____|\_______▌} {▐______|\______▌} {▐_______|\_____▌} {▐________|\____▌} {▐_________|\___▌} {▐__________|\__▌} {▐___________|\_▌} {▐____________|\▌} ▐____________/|▌ ▐___________/|_▌ ▐__________/|__▌ ▐_________/|___▌ ▐________/|____▌ ▐_______/|_____▌ ▐______/|______▌ ▐_____/|_______▌ ▐____/|________▌ ▐___/|_________▌ ▐__/|__________▌ ▐_/|___________▌ ▐/|____________▌ }}
        dqpb { 100 { d q p b }}
        grenade { 80 { {،   } {′   } { ´ } { ‾ } {  ⸌} {  ⸊} {  |} {  ⁎} {  ⁕} { ෴ } {  ⁓} {   } {   } {   } }}
        point { 125 { ∙∙∙ ●∙∙ ∙●∙ ∙∙● ∙∙∙ }}
        layer { 150 { - = ≡ }}
        betaWave { 80 { ρββββββ βρβββββ ββρββββ βββρβββ ββββρββ βββββρβ ββββββρ }}
    }
}
proc ::Spinner::Info {{spinnerName :random:}} {
    # Returns info about a spinner, or a random one if name ":random:" is given
    variable SPINNERS

    if {$spinnerName eq ":random:"} {
        set names [array names SPINNERS]
        set n [expr {int(rand() * [llength $names])}]
        set spinnerName [lindex $names $n]
    }

    lassign $SPINNERS($spinnerName) interval frames
    set maxWidth [::tcl::mathfunc::max {*}[lmap f $frames {string length $f}]]
    return [list $spinnerName $interval $frames $maxWidth]
}
proc ::Spinner::Start {name widget_or_textvar} {
    variable SPINNERS
    variable NextID
    variable ACTIVE

    set id [incr NextID]

    if {[info commands winfo] ne "" && [winfo exists $widget_or_textvar]} {
        set textvar [$widget_or_textvar cget -textvar]
    } else {
        set textvar $widget_or_textvar
    }

    set ACTIVE($id) go
    ::Spinner::_Go $id $textvar $name 0
    return $id
}
proc ::Spinner::Stop {{id ""}} {
    # Stop a single banner or multiple banners
    variable ACTIVE
    if {$id eq ""} {
        array unset ACTIVE
    } else {
        array unset ACTIVE $id
    }
}
proc ::Spinner::_Go {id textvar spinnerName idx} {
    # Internal routine to update spinner $id

    variable ACTIVE
    variable SPINNERS

    if {! [info exists ACTIVE($id)]} return

    lassign $SPINNERS($spinnerName) interval frames

    set $textvar [lindex $frames $idx]
    set idx [expr {($idx + 1) % [llength $frames]}]
    after $interval [list ::Spinner::_Go $id $textvar $spinnerName $idx]
}

################################################################
################################################################
#
# Demo code
#

package require Tk
proc DoDisplay {} {
    wm title . "Spinner Demo"
    set font {Helvetica 36}
    set fontBold {Courier 36 bold}

    set numColumns 3
    set widgets {}
    set row -1

    set names [lsort -dictionary [array names ::Spinner::SPINNERS]]
    set n [lsearch $names "shark"]
    set names [concat [lreplace $names $n $n] shark]
    foreach spinnerName $names {
        incr row
        label .title$row -text $spinnerName -font $font
        label .spin$row -textvar ::spinner($row) -font $fontBold \
            -width 10 -bd 2 -relief solid
        ::Spinner::Start $spinnerName .spin$row
        lassign [::Spinner::Info $spinnerName] . . . maxWidth
        lappend widths $maxWidth

        lappend widgets .title$row .spin$row
        if {[llength $widgets] >= 2*$numColumns} {
            grid {*}$widgets
            set widgets {}
        }
    }
    if {$widgets ne {}} {
        grid {*}$widgets
    }
    # The shark spinner needs extra space
    grid config .spin$row -columnspan 2 -sticky w
    .spin$row config -width 16
}
DoDisplay

return

arjen - 2019-10-02 07:20:23

Very nice! Two remarks though: when I ran it on my laptop, the window was larger than the screen and the font did not have all the glyphs, it seems as some spinners only showed a single question mark.


arjen - 2019-10-02 07:22:03

Hm, looking at the source code as I saved it, it might be due to the way I saved the code (copy-paste) - a lot of the characters are simply question marks.