refactor: split every monolithic module into focused submodules
Each source file that had grown beyond a single concern is replaced by an identically-named directory containing focused submodules. `src/event_loop/mod.rs` (878 lines) becomes a directory with clipboard, context_menu, cursor_shape, drag, focus, handlers, invalidation, overlays_reconcile, repeat, run, surface, text_editing, and tooltip. Every widget, input handler, and theme component follows the same split. Public interfaces are unchanged — only the internal file layout moves. image bumped from 0.25.2 to 0.25.9.
This commit is contained in:
@@ -0,0 +1,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;
|
||||
@@ -0,0 +1,38 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-only
|
||||
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
|
||||
|
||||
use super::*;
|
||||
|
||||
#[ test ]
|
||||
fn defaults()
|
||||
{
|
||||
let s = spinner();
|
||||
assert_eq!( s.phase, 0.0 );
|
||||
assert_eq!( s.size, theme::SIZE );
|
||||
assert_eq!( s.stroke_w, theme::STROKE_W );
|
||||
}
|
||||
|
||||
#[ test ]
|
||||
fn builders_apply()
|
||||
{
|
||||
let s = spinner().phase( 0.42 ).size( 64.0 ).stroke_width( 5.0 );
|
||||
assert_eq!( s.phase, 0.42 );
|
||||
assert_eq!( s.size, 64.0 );
|
||||
assert_eq!( s.stroke_w, 5.0 );
|
||||
}
|
||||
|
||||
#[ test ]
|
||||
fn preferred_size_clamps_to_max_width()
|
||||
{
|
||||
let s = spinner().size( 100.0 );
|
||||
assert_eq!( s.preferred_size( 40.0 ), ( 40.0, 40.0 ) );
|
||||
assert_eq!( s.preferred_size( 200.0 ), ( 100.0, 100.0 ) );
|
||||
}
|
||||
|
||||
#[ test ]
|
||||
fn preferred_size_is_square()
|
||||
{
|
||||
let s = spinner();
|
||||
let ( w, h ) = s.preferred_size( 999.0 );
|
||||
assert_eq!( w, h );
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-only
|
||||
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
|
||||
|
||||
use crate::types::Color;
|
||||
pub fn fill() -> Color { crate::theme::palette().accent }
|
||||
pub fn track() -> Color
|
||||
{
|
||||
// Quarter-opacity copy of the accent so the moving arc reads
|
||||
// against a faint guide ring instead of empty surface.
|
||||
let a = crate::theme::palette().accent;
|
||||
Color::rgba( a.r, a.g, a.b, 0.20 )
|
||||
}
|
||||
pub const SIZE: f32 = 32.0;
|
||||
pub const STROKE_W: f32 = 3.0;
|
||||
/// Fraction of the full circle that the moving arc covers.
|
||||
pub const ARC_FRAC: f32 = 0.30;
|
||||
/// Number of straight segments used to approximate one full circle.
|
||||
/// 36 is enough for a smooth arc at the default 32 px diameter and
|
||||
/// keeps the per-frame draw call count low.
|
||||
pub const SEGMENTS: u32 = 36;
|
||||
Reference in New Issue
Block a user