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,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 )
|
||||
}
|
||||
}
|
||||
@@ -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 );
|
||||
}
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user