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

206
src/widget/toggle/mod.rs Normal file
View File

@@ -0,0 +1,206 @@
// 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;
/// A two-state on / off switch.
///
/// Renders as a horizontal pill with a circular thumb that slides between the
/// off (left, divider colour) and on (right, accent colour) positions. The
/// widget is stateless: the application owns `value` and rebuilds the toggle
/// from its current state on every frame. Tapping or pressing Enter / Space
/// while focused emits the message configured with [`Self::on_toggle`] — the
/// app's `update` is then expected to flip the bool and re-render.
///
/// ```rust,no_run
/// # use ltk::{ toggle, Toggle };
/// # #[ derive( Clone ) ] enum Msg { ToggleWifi }
/// # struct App { wifi_enabled: bool }
/// # impl App { fn _ex( &self ) -> Toggle<Msg> {
/// // In view():
/// toggle( self.wifi_enabled )
/// .label( "Wi-Fi" )
/// .on_toggle( Msg::ToggleWifi )
/// # }}
/// ```
///
/// See also [`Checkbox`](super::checkbox::Checkbox) for binary opt-in
/// controls (terms acceptance, multi-select form fields) where the
/// pill-style affordance is too prominent.
pub struct Toggle<Msg: Clone>
{
/// Current on / off state. Drawn from this field every frame; the
/// runtime never mutates it.
pub value: bool,
/// Message emitted on activation. `None` leaves the toggle inert (it
/// still renders and takes focus, but does nothing on press).
pub on_toggle: Option<Msg>,
/// Optional label drawn to the left of the track.
pub label: Option<String>,
/// Optional stable identifier for focus management.
pub id: Option<WidgetId>,
}
impl<Msg: Clone> Toggle<Msg>
{
/// Create a toggle in the given state, with no label and no callback.
///
/// Wire activation through [`Self::on_toggle`] before adding it to a
/// widget tree, otherwise the toggle is decorative.
pub fn new( value: bool ) -> Self
{
Self { value, on_toggle: None, label: None, id: None }
}
/// Set the message emitted when the toggle is activated (tap, Enter or
/// Space while focused). The application's `update` is responsible for
/// flipping `value` in response.
pub fn on_toggle( mut self, msg: Msg ) -> Self
{
self.on_toggle = Some( msg );
self
}
/// Set a text label rendered to the left of the track. The toggle's
/// preferred width grows to fit `label_width + gap + track_width`,
/// capped at the parent-supplied `max_width`.
pub fn label( mut self, label: impl Into<String> ) -> Self
{
self.label = Some( label.into() );
self
}
/// Assign a stable identifier so the application can target this
/// toggle through [`crate::App::take_focus_request`].
pub fn id( mut self, id: WidgetId ) -> Self
{
self.id = Some( id );
self
}
/// Return the preferred `(width, height)` given available `max_width`.
///
/// Width is `track_width` for an unlabelled toggle, or
/// `label_width + gap + track_width` (clamped to `max_width`) when a
/// label is set. Height is the theme-defined row height.
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 );
( text_w + theme::GAP + theme::TRACK_W ).min( max_width )
} else {
theme::TRACK_W.min( max_width )
};
( w, theme::HEIGHT )
}
/// Bounding box of everything painted at `rect` across all states. The
/// focus ring is drawn as `track_rect.expand( FOCUS_W + 2 )` with a stroke
/// of width `FOCUS_W`, so it extends `FOCUS_W + 2 + FOCUS_W/2 ≈ 6.5 px`
/// beyond the track. Since the track sits at the right edge of `rect`,
/// the ring can extend that far outside the widget's layout rect.
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 track_x = if let Some( ref label ) = self.label
{
let text_y = rect.y + ( rect.height + theme::FONT_SIZE ) / 2.0 - 2.0;
canvas.draw_text( label, rect.x, text_y, theme::FONT_SIZE, theme::label_color() );
rect.x + rect.width - theme::TRACK_W
} else {
rect.x + ( rect.width - theme::TRACK_W ) / 2.0
};
let track_y = rect.y + ( rect.height - theme::TRACK_H ) / 2.0;
let track_r = theme::TRACK_H / 2.0;
let track_rect = Rect
{
x: track_x,
y: track_y,
width: theme::TRACK_W,
height: theme::TRACK_H,
};
let track_color = if self.value { theme::track_on() } else { theme::track_off() };
canvas.fill_rect( track_rect, track_color, track_r );
let thumb_pad = ( theme::TRACK_H - theme::THUMB_SIZE ) / 2.0;
let thumb_cx = if self.value
{
track_x + theme::TRACK_W - thumb_pad - theme::THUMB_SIZE / 2.0
} else {
track_x + thumb_pad + theme::THUMB_SIZE / 2.0
};
let thumb_cy = track_y + theme::TRACK_H / 2.0;
let thumb_r = theme::THUMB_SIZE / 2.0;
let thumb_rect = Rect
{
x: thumb_cx - thumb_r,
y: thumb_cy - thumb_r,
width: theme::THUMB_SIZE,
height: theme::THUMB_SIZE,
};
canvas.fill_rect( thumb_rect, theme::thumb(), thumb_r );
canvas.stroke_rect( thumb_rect, theme::thumb_border(), theme::THUMB_BORDER_W, thumb_r );
if focused
{
let ring = track_rect.expand( theme::FOCUS_W + 2.0 );
canvas.stroke_rect( ring, theme::focus_color(), theme::FOCUS_W, track_r + theme::FOCUS_W + 2.0 );
}
}
pub( crate ) fn map_msg<U>( self, f: &super::MapFn<Msg, U> ) -> Toggle<U>
where
U: Clone + 'static,
Msg: 'static,
{
Toggle
{
value: self.value,
on_toggle: self.on_toggle.map( |m| ( *f )( m ) ),
label: self.label,
id: self.id,
}
}
}
/// Create a [`Toggle`] in the given state.
///
/// Shorthand for [`Toggle::new`]. Wire activation with [`Toggle::on_toggle`]
/// and an optional label with [`Toggle::label`]:
///
/// ```rust,no_run
/// # use ltk::{ toggle, Toggle };
/// # #[ derive( Clone ) ] enum Msg { ToggleWifi }
/// # struct App { wifi_enabled: bool }
/// # impl App { fn _ex( &self ) -> Toggle<Msg> {
/// toggle( self.wifi_enabled )
/// .label( "Wi-Fi" )
/// .on_toggle( Msg::ToggleWifi )
/// # }}
/// ```
pub fn toggle<Msg: Clone>( value: bool ) -> Toggle<Msg>
{
Toggle::new( value )
}
impl<Msg: Clone + 'static> From<Toggle<Msg>> for Element<Msg>
{
fn from( t: Toggle<Msg> ) -> Self
{
Element::Toggle( t )
}
}

View File

@@ -0,0 +1,20 @@
// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
use super::*;
#[test]
fn toggle_default_state()
{
let t = toggle::<()>( true );
assert!( t.value );
assert!( t.on_toggle.is_none() );
assert!( t.label.is_none() );
}
#[test]
fn toggle_off_state()
{
let t = toggle::<()>( false );
assert!( !t.value );
}

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 track_off() -> Color { crate::theme::palette().divider }
pub fn track_on() -> Color { crate::theme::palette().accent }
/// Thumb — uses the page-background colour so it reads as the
/// inverse of the track fill regardless of mode.
pub fn thumb() -> Color { crate::theme::palette().bg }
pub fn thumb_border() -> Color { crate::theme::palette().text_primary }
pub fn focus_color() -> Color { crate::theme::palette().accent }
pub fn label_color() -> Color { crate::theme::palette().text_primary }
pub const TRACK_W: f32 = 52.0;
pub const TRACK_H: f32 = 30.0;
pub const THUMB_SIZE: f32 = 24.0;
pub const HEIGHT: f32 = 48.0;
pub const GAP: f32 = 12.0;
pub const FOCUS_W: f32 = 3.0;
pub const THUMB_BORDER_W: f32 = 1.0;
pub const FONT_SIZE: f32 = 16.0;