ltk/widget/spinner/mod.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167
// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
//! Spinner — indeterminate progress indicator.
//!
//! A rotating arc that the renderer draws by stroking a fraction of a
//! circle and offsetting its starting angle by [`Spinner::phase`]. The
//! widget is *stateless*: the application owns the phase variable and
//! advances it from a clock or animation tick. Pair with
//! [`App::is_animating`](crate::App::is_animating) so the run loop keeps
//! requesting redraws while the spinner is on screen.
//!
//! ```rust,no_run
//! # use ltk::{ row, spinner, text, Element };
//! # #[ derive( Clone ) ] enum Msg {}
//! # struct App { tick: f32 }
//! # impl App { fn _ex( &self ) -> Element<Msg> {
//! // In view():
//! row()
//! .push( spinner().phase( self.tick ) )
//! .push( text( "Loading…" ) )
//! .into()
//! # }}
//! ```
use crate::types::{ Color, Rect };
use crate::render::Canvas;
use super::Element;
mod theme;
/// An indeterminate progress spinner.
///
/// Renders a rotating arc inside a square layout rect. The application
/// drives the rotation by passing a monotonically-increasing `phase`
/// value (any units — only the fractional part of `phase` is used).
pub struct Spinner
{
/// Rotation phase. Only the fractional part is consumed, so any
/// monotonically increasing source works (`elapsed.as_secs_f32()`,
/// frame count divided by FPS, etc.).
pub phase: f32,
/// Arc / ring colour. Defaults to the theme's `accent` palette slot.
pub color: Color,
/// Square diameter in logical pixels. Both width and height of the
/// laid-out rect target this size.
pub size: f32,
/// Stroke width of the arc and the dim guide ring.
pub stroke_w: f32,
}
impl Spinner
{
/// Create a spinner with the theme's accent colour and default size.
pub fn new() -> Self
{
Self
{
phase: 0.0,
color: theme::fill(),
size: theme::SIZE,
stroke_w: theme::STROKE_W,
}
}
/// Set the rotation phase. The widget consumes only the fractional
/// part, so callers can pass an unbounded monotonic clock value.
pub fn phase( mut self, p: f32 ) -> Self
{
self.phase = p;
self
}
/// Override the arc colour.
pub fn color( mut self, c: Color ) -> Self
{
self.color = c;
self
}
/// Set the square diameter in logical pixels.
pub fn size( mut self, s: f32 ) -> Self
{
self.size = s;
self
}
/// Set the arc / ring stroke width in logical pixels.
pub fn stroke_width( mut self, w: f32 ) -> Self
{
self.stroke_w = w;
self
}
/// Return the preferred `(width, height)` — the spinner is square.
pub fn preferred_size( &self, max_width: f32 ) -> ( f32, f32 )
{
let s = self.size.min( max_width );
( s, s )
}
/// Draw the spinner into `canvas` at `rect`. The arc is centred on
/// `rect`'s minor diameter so the widget renders correctly even when
/// laid out into a non-square cell.
pub fn draw( &self, canvas: &mut Canvas, rect: Rect )
{
let cx = rect.x + rect.width * 0.5;
let cy = rect.y + rect.height * 0.5;
let r = ( rect.width.min( rect.height ) - self.stroke_w ) * 0.5;
if r <= 0.0 { return; }
let track = theme::track();
let n = theme::SEGMENTS as i32;
let two_pi = std::f32::consts::TAU;
let dim = two_pi / n as f32;
// Dim guide ring (full circle as polyline).
for i in 0..n
{
let a0 = i as f32 * dim;
let a1 = ( i + 1 ) as f32 * dim;
let x0 = cx + r * a0.cos();
let y0 = cy + r * a0.sin();
let x1 = cx + r * a1.cos();
let y1 = cy + r * a1.sin();
canvas.draw_line( x0, y0, x1, y1, track, self.stroke_w );
}
// Moving arc. `phase` is consumed modulo 1.0.
let frac = self.phase - self.phase.floor();
let start_ang = frac * two_pi;
let end_ang = start_ang + theme::ARC_FRAC * two_pi;
let arc_segs = ( ( theme::ARC_FRAC * n as f32 ).round() as i32 ).max( 1 );
for i in 0..arc_segs
{
let a0 = start_ang + i as f32 * dim;
let a1 = ( start_ang + ( i + 1 ) as f32 * dim ).min( end_ang );
let x0 = cx + r * a0.cos();
let y0 = cy + r * a0.sin();
let x1 = cx + r * a1.cos();
let y1 = cy + r * a1.sin();
canvas.draw_line( x0, y0, x1, y1, self.color, self.stroke_w );
}
}
}
impl Default for Spinner
{
fn default() -> Self { Self::new() }
}
/// Create a [`Spinner`] with default size and theme colour.
pub fn spinner() -> Spinner
{
Spinner::new()
}
impl<Msg: Clone> From<Spinner> for Element<Msg>
{
fn from( s: Spinner ) -> Self
{
Element::Spinner( s )
}
}
#[ cfg( test ) ]
mod tests;