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
+177
View File
@@ -0,0 +1,177 @@
// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
use crate::types::{ Rect, WidgetId };
use crate::render::Canvas;
use super::Element;
mod theme;
#[cfg(test)]
mod tests;
/// One option inside a mutually-exclusive group.
///
/// Renders a circle with a centred dot when selected. Unlike
/// [`Checkbox`](super::checkbox::Checkbox), a radio is meaningful only as
/// part of a group: the group is the application's responsibility — define
/// an enum for the choices, store the current value, and build one `Radio`
/// per variant with `selected = current == this_variant`.
///
/// ```rust,no_run
/// # use ltk::{ column, radio, Element };
/// #[ derive( Clone, PartialEq ) ]
/// enum Priority { Low, Medium, High }
/// # #[ derive( Clone ) ] enum Msg { SetPriority( Priority ) }
/// # struct App { priority: Priority }
/// # impl App { fn _ex( &self ) -> Element<Msg> {
/// // In view():
/// column()
/// .push( radio( self.priority == Priority::Low ).label( "Low" ).on_select( Msg::SetPriority( Priority::Low ) ) )
/// .push( radio( self.priority == Priority::Medium ).label( "Medium" ).on_select( Msg::SetPriority( Priority::Medium ) ) )
/// .push( radio( self.priority == Priority::High ).label( "High" ).on_select( Msg::SetPriority( Priority::High ) ) )
/// .into()
/// # }}
/// ```
///
/// `ltk` does not enforce mutual exclusion automatically; the application's
/// `update` decides which variant becomes active when a radio is selected.
pub struct Radio<Msg: Clone>
{
/// Whether this option is currently selected. Drawn from this field
/// every frame.
pub selected: bool,
/// Message emitted when the user picks this option. `None` leaves the
/// radio inert.
pub on_select: Option<Msg>,
/// Optional label drawn to the right of the circle.
pub label: Option<String>,
/// Optional stable identifier for focus management.
pub id: Option<WidgetId>,
}
impl<Msg: Clone> Radio<Msg>
{
/// Create a radio in the given state, with no label and no callback.
pub fn new( selected: bool ) -> Self
{
Self { selected, on_select: None, label: None, id: None }
}
/// Set the message emitted when this option is picked. The
/// application's `update` is responsible for flipping the group's
/// current value.
pub fn on_select( mut self, msg: Msg ) -> Self
{
self.on_select = Some( msg );
self
}
/// Set a text label rendered to the right of the circle.
pub fn label( mut self, label: impl Into<String> ) -> Self
{
self.label = Some( label.into() );
self
}
/// Assign a stable identifier for focus management.
pub fn id( mut self, id: WidgetId ) -> Self
{
self.id = Some( id );
self
}
pub fn preferred_size( &self, max_width: f32, canvas: &Canvas ) -> (f32, f32)
{
let w = if let Some( ref label ) = self.label
{
let text_w = canvas.measure_text( label, theme::FONT_SIZE );
( theme::OUTER_SIZE + theme::GAP + text_w ).min( max_width )
} else {
theme::OUTER_SIZE.min( max_width )
};
( w, theme::HEIGHT )
}
/// Focus ring on the outer circle extends `FOCUS_W + 2 + FOCUS_W/2 ≈ 6.5 px`
/// beyond the circle (which sits flush with the widget's left edge).
pub fn paint_bounds( &self, rect: Rect ) -> Rect
{
rect.expand( theme::FOCUS_W + 2.0 + theme::FOCUS_W * 0.5 + 1.0 )
}
pub fn draw( &self, canvas: &mut Canvas, rect: Rect, focused: bool )
{
let circle_y = rect.y + ( rect.height - theme::OUTER_SIZE ) / 2.0;
let outer_r = theme::OUTER_SIZE / 2.0;
let outer_rect = Rect
{
x: rect.x,
y: circle_y,
width: theme::OUTER_SIZE,
height: theme::OUTER_SIZE,
};
if self.selected
{
canvas.fill_rect( outer_rect, theme::selected(), outer_r );
let dot_r = theme::DOT_SIZE / 2.0;
let cx = rect.x + outer_r;
let cy = circle_y + outer_r;
let dot_rect = Rect
{
x: cx - dot_r,
y: cy - dot_r,
width: theme::DOT_SIZE,
height: theme::DOT_SIZE,
};
canvas.fill_rect( dot_rect, theme::dot_color(), dot_r );
} else {
canvas.stroke_rect( outer_rect, theme::ring_color(), theme::BORDER_W, outer_r );
}
if focused
{
let ring = outer_rect.expand( theme::FOCUS_W + 2.0 );
canvas.stroke_rect( ring, theme::focus_color(), theme::FOCUS_W, outer_r + theme::FOCUS_W + 2.0 );
}
if let Some( ref label ) = self.label
{
let text_x = rect.x + theme::OUTER_SIZE + theme::GAP;
let text_y = rect.y + ( rect.height + theme::FONT_SIZE ) / 2.0 - 2.0;
canvas.draw_text( label, text_x, text_y, theme::FONT_SIZE, theme::label_color() );
}
}
pub( crate ) fn map_msg<U>( self, f: &super::MapFn<Msg, U> ) -> Radio<U>
where
U: Clone + 'static,
Msg: 'static,
{
Radio
{
selected: self.selected,
on_select: self.on_select.map( |m| ( *f )( m ) ),
label: self.label,
id: self.id,
}
}
}
/// Create a [`Radio`] option in the given state.
///
/// Shorthand for [`Radio::new`]. See the [`Radio`] type-level docs for
/// the full mutual-exclusion pattern across multiple options.
pub fn radio<Msg: Clone>( selected: bool ) -> Radio<Msg>
{
Radio::new( selected )
}
impl<Msg: Clone + 'static> From<Radio<Msg>> for Element<Msg>
{
fn from( r: Radio<Msg> ) -> Self
{
Element::Radio( r )
}
}
+19
View File
@@ -0,0 +1,19 @@
// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
use super::*;
#[test]
fn radio_default_state()
{
let r = radio::<()>( true );
assert!( r.selected );
assert!( r.on_select.is_none() );
}
#[test]
fn radio_unselected()
{
let r = radio::<()>( false );
assert!( !r.selected );
}
+18
View File
@@ -0,0 +1,18 @@
// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
use crate::types::Color;
pub fn ring_color() -> Color { crate::theme::palette().divider }
pub fn selected() -> Color { crate::theme::palette().accent }
/// Inner dot — uses the page-background colour so it reads as
/// the inverse of the accent fill regardless of mode.
pub fn dot_color() -> Color { crate::theme::palette().bg }
pub fn focus_color() -> Color { crate::theme::palette().accent }
pub fn label_color() -> Color { crate::theme::palette().text_primary }
pub const OUTER_SIZE: f32 = 24.0;
pub const DOT_SIZE: f32 = 12.0;
pub const BORDER_W: f32 = 2.0;
pub const GAP: f32 = 12.0;
pub const HEIGHT: f32 = 48.0;
pub const FOCUS_W: f32 = 3.0;
pub const FONT_SIZE: f32 = 16.0;