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;