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:
206
src/widget/toggle/mod.rs
Normal file
206
src/widget/toggle/mod.rs
Normal 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 )
|
||||
}
|
||||
}
|
||||
20
src/widget/toggle/tests.rs
Normal file
20
src/widget/toggle/tests.rs
Normal 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 );
|
||||
}
|
||||
20
src/widget/toggle/theme.rs
Normal file
20
src/widget/toggle/theme.rs
Normal 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;
|
||||
Reference in New Issue
Block a user