Tryst::Spinner
Inherits Tryst::OwnerDrawnWidget < Reference < Object
An activity ring for tryst: indeterminate (a continuously rotating, breathing arc - the "something's happening, no ETA" case) or determinate (a fixed arc from 0 to a 0.0-1.0 value, with an optional centered percentage). One shard, one widget - determinate mode is just what happens when #value is set to something other than nil, not a separate widget (see #value='s own doc comment). Built on OwnerDrawnWidget and rendered through tryst-vector: an antialiased stroke with round caps is the whole visual point, which is exactly what Tk's own indeterminate ttk::progressbar has neither of.
spinner = Tryst::Spinner.new(app) # indeterminate
spinner.pack
sync = Tryst::Spinner.new(app, value: 0.0, show_value: true) # determinate
sync.pack
sync.value = 0.65
Nothing about canvas, Surface, or ThorVG is part of this class's own public surface - same boundary OwnerDrawnWidget itself draws (see its own doc comment on why #canvas is protected).
Constants
~30fps, not 60 - plenty smooth for a rotating/breathing arc (unlike drag interactions elsewhere in this codebase, nothing about this animation needs to track input latency), and it runs for as long as the spinner is on screen rather than for one bounded interaction - see the README's own CPU-at-idle-spin cost for why that tradeoff matters here specifically.
Radial margin between the stroke's own outer edge and the canvas's own bounds - without it, a stroke sized right up to the edge gets AA-clipped by the buffer's own bounds the same way ValueSlider's bubble chrome once was (see that shard's own #initialize comment on the exact failure mode this avoids).
Indeterminate motion: the arc's leading edge completes one full turn every ROTATION_PERIOD_MS (continuous, unbounded), while its own length breathes between MIN/MAX_SWEEP_DEG once every SWEEP_PERIOD_MS - two independent cycles is what keeps it reading as "alive" rather than a wedge just spinning in place.
Default thickness as a fraction of size, when thickness: isn't given explicitly - matches the design mock's own 16/24/48px -> ~2/3/6px progression.
Constructors
size/thickness in logical pixels. value: nil (the default) starts indeterminate; anything else is clamped to [0.0, 1.0] and starts determinate. accent: nil uses the active ttk theme's own accent color, or a "#rrggbb" hex string to override it (same convention as ValueSlider's own accent:). font: only matters when show_value is true.
Instance methods
Releases the underlying canvas (and its Photo, if any) now, rather than waiting for a collection - same contract as Photo#delete. Bind-callback cleanup for #canvas's own path happens for free (see App's setup_destroy_cleanup, installed unconditionally for every widget); only this class's own extra state (running tweens) needs explicit teardown here.
Subclasses draw here - called after every resize and every state change (#hover?/#pressed?/#focused?/#disabled? all changing). Given no arguments deliberately: #canvas's own current width/height (via #canvas.width/#canvas.height, i.e. real winfo queries) are always the authority, not a value that could go stale between when a resize fired and when this actually runs.
Setting nil switches to indeterminate (restarting the sweep from its own beginning); setting a Float64 (clamped to [0.0, 1.0]) switches to/stays in determinate mode. A determinate-to-determinate change animates smoothly (matching CircularProgress's own #value=); the very first determinate value, or one arriving right after an indeterminate stretch, jumps straight there - there is no meaningful "previous position" to animate from in either case.