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:
2026-05-15 23:46:56 +02:00
parent 3d237039c6
commit 4aa3480b64
155 changed files with 13832 additions and 13035 deletions
+167
View File
@@ -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;
+38
View File
@@ -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 );
}
+20
View File
@@ -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;