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

423
src/widget/slider/mod.rs Normal file
View File

@@ -0,0 +1,423 @@
// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
use std::sync::Arc;
use crate::types::{ Point, Rect };
use crate::render::Canvas;
use super::Element;
mod theme;
#[ cfg( test ) ]
mod tests;
/// Which axis a slider tracks. Used by input dispatch to pick the right
/// `value_from_*_in_rect` formula for a [`Slider`] (horizontal) or
/// [`crate::widget::vslider::VSlider`] (vertical).
#[ derive( Debug, Clone, Copy, PartialEq, Eq ) ]
pub enum SliderAxis
{
Horizontal,
Vertical,
}
/// Compute the slider value `[0.0, 1.0]` from a pointer position within a
/// slider's layout rect, dispatching to the axis-specific formula.
///
/// Exposed alongside [`value_from_x_in_rect`] so `input.rs` can drive both
/// [`Slider`] and [`crate::widget::vslider::VSlider`] through the same call
/// site by consulting the [`SliderAxis`] stored in the widget's handler
/// snapshot.
pub fn value_from_pos_in_rect( rect: Rect, pos: Point, axis: SliderAxis ) -> f32
{
match axis
{
SliderAxis::Horizontal => value_from_x_in_rect( rect, pos.x ),
SliderAxis::Vertical => crate::widget::vslider::value_from_y_in_rect( rect, pos.y ),
}
}
/// Intersect `inner` with the `saved` outer clip and return the rect
/// list to install with [`crate::render::Canvas::set_clip_rects`].
///
/// `saved` is the snapshot returned by
/// [`crate::render::Canvas::clip_bounds`]: empty means "no clip is
/// installed" (paint everywhere), non-empty is the outer scissor list
/// — typically the partial-redraw damage rects.
///
/// `Slider` / [`crate::widget::vslider::VSlider`] use this when they
/// need to clip the active-side fill paint to a band tighter than the
/// widget rect. Calling `set_clip_rects( &[ band ] )` directly would
/// REPLACE the outer clip, causing the fill to repaint outside the
/// damage region during partial redraws and clobbering pixels (most
/// visibly the thumb's white interior left from the previous frame).
/// Routing through this helper preserves the outer clip by
/// intersecting with it.
///
/// Returns `Vec::new()` when there is no overlap; callers must skip
/// the paint entirely in that case — installing an empty rect list
/// means "no clip" which would paint outside the damage region.
pub( crate ) fn intersect_clip( saved: &[ Rect ], inner: Rect ) -> Vec<Rect>
{
if saved.is_empty()
{
return vec![ inner ];
}
saved.iter().filter_map( |r|
{
let x0 = inner.x.max( r.x );
let y0 = inner.y.max( r.y );
let x1 = ( inner.x + inner.width ).min( r.x + r.width );
let y1 = ( inner.y + inner.height ).min( r.y + r.height );
if x1 <= x0 || y1 <= y0
{
None
}
else
{
Some( Rect { x: x0, y: y0, width: x1 - x0, height: y1 - y0 } )
}
} ).collect()
}
/// Compute the slider value `[0.0, 1.0]` from a tap/drag x position within a
/// slider's layout rect. Pure — depends only on `theme::THUMB_SIZE`. Lifted
/// out of [`Slider`] so input handlers can call it directly from
/// [`crate::widget::LaidOutWidget`] without needing the [`Element`] tree.
pub fn value_from_x_in_rect( rect: Rect, x: f32 ) -> f32
{
let pad = theme::THUMB_SIZE / 2.0;
let track_start = rect.x + pad;
let track_end = rect.x + rect.width - pad;
let track_w = ( track_end - track_start ).max( 1.0 );
( ( x - track_start ) / track_w ).clamp( 0.0, 1.0 )
}
/// A horizontal slider for selecting a value in a range.
///
/// The track defaults to a theme-resolved surface; pass
/// [`Self::track_paint`] to override with a custom
/// [`Paint`](crate::theme::Paint) — typically a multi-stop linear
/// gradient for a hue / spectrum picker. When `track_paint` is set
/// the active-side fill is suppressed (the spectrum already
/// communicates the position visually through colour).
///
/// ```rust,no_run
/// # use ltk::{ slider, Slider };
/// # #[ derive( Clone ) ] enum Msg { SetBrightness( f32 ) }
/// # struct App { brightness: f32 }
/// # impl App { fn _ex( &self ) -> Slider<Msg> {
/// slider( self.brightness )
/// .on_change( |v| Msg::SetBrightness( v ) )
/// # }}
/// ```
pub struct Slider<Msg: Clone>
{
/// Current value in `[0.0, 1.0]`.
pub value: f32,
/// Callback invoked with the new value when the slider is dragged.
/// `Arc` (not `Box`) so the layout pass can clone it into the per-leaf
/// handler snapshot for O(1) dispatch on input events.
pub on_change: Option<Arc<dyn Fn(f32) -> Msg>>,
/// Theme slot id for the track background pill. Defaults to the
/// generic `surface-slider-track`; override per-instance to opt
/// into the `-flat` (no per-surface backdrop) variant when the
/// slider already lives inside a panel-wide blur, or any other
/// custom slot.
pub track_surface: &'static str,
/// Theme slot id for the rising / leftward fill. Same shape as
/// [`Self::track_surface`] but for the active portion.
pub fill_surface: &'static str,
/// Paint the thumb with the active palette's `accent` colour and
/// a thicker white border (accent inner pill + 4 px white ring)
/// instead of the default white-on-text-primary thumb.
pub accent_thumb: bool,
/// Override the track paint with a custom
/// [`Paint`](crate::theme::Paint) — typically a
/// [`Paint::Linear`](crate::theme::Paint::Linear) for spectrum /
/// hue pickers. When set, the active-side fill is suppressed
/// because a spectrum already conveys position information
/// through colour and the thumb shows where the user is.
pub track_paint: Option<crate::theme::Paint>,
}
impl<Msg: Clone> Slider<Msg>
{
/// Create a slider with the given value (clamped to `[0.0, 1.0]`).
pub fn new( value: f32 ) -> Self
{
Self
{
value: value.clamp( 0.0, 1.0 ),
on_change: None,
track_surface: theme::SURFACE_TRACK,
fill_surface: theme::SURFACE_FILL,
accent_thumb: false,
track_paint: None,
}
}
/// Override the track paint with a custom
/// [`Paint`](crate::theme::Paint) — e.g. a multi-stop linear
/// gradient for a hue / spectrum picker. Disables the active-
/// side fill (the spectrum already encodes the position
/// visually).
pub fn track_paint( mut self, paint: crate::theme::Paint ) -> Self
{
self.track_paint = Some( paint );
self
}
/// Set the callback invoked when the slider value changes.
pub fn on_change( mut self, f: impl Fn(f32) -> Msg + 'static ) -> Self
{
self.on_change = Some( Arc::new( f ) );
self
}
/// Override the theme slot id used to paint the track background
/// — pass `surface-slider-track-flat` (or any other surface) to
/// drop the per-instance backdrop blur when the slider already
/// lives inside a panel-wide blur. See
/// [`crate::widget::vslider::VSlider::track_surface`] for the use
/// case.
pub fn track_surface( mut self, id: &'static str ) -> Self
{
self.track_surface = id;
self
}
/// Override the theme slot id used to paint the fill. Mirrors
/// [`Self::track_surface`].
pub fn fill_surface( mut self, id: &'static str ) -> Self
{
self.fill_surface = id;
self
}
/// Switch the thumb to the accent-pill style: accent-coloured inner
/// + 4 px white border. Default thumb stays white-on-text-primary
/// for shells that haven't opted in.
pub fn accent_thumb( mut self, on: bool ) -> Self
{
self.accent_thumb = on;
self
}
/// Return the preferred `(width, height)`.
pub fn preferred_size( &self, max_width: f32, _canvas: &Canvas ) -> (f32, f32)
{
( max_width, theme::HEIGHT )
}
/// Compute the value `[0.0, 1.0]` from a tap/drag x position within `rect`.
pub fn value_from_x( &self, rect: Rect, x: f32 ) -> f32
{
value_from_x_in_rect( rect, x )
}
/// Thumb border stroke is centered on the thumb edge (which touches the
/// widget's left/right edges at value 0 / 1), so half the stroke width
/// plus ~1 px of antialiasing sits outside `rect`.
pub fn paint_bounds( &self, rect: Rect ) -> Rect
{
let border_w = if self.accent_thumb { theme::ACCENT_THUMB_BORDER_W } else { theme::THUMB_BORDER_W };
rect.expand( ( border_w * 0.5 + 1.0 ).max( surface_shadow_margin( self.track_surface ) ) )
}
/// Draw the slider into `canvas` at `rect`.
///
/// Track + fill paint through theme slots ([`Self::track_surface`]
/// / [`Self::fill_surface`]) when those slots resolve in the
/// active theme — the track pill spans the full inner width and
/// the fill is anchored to the same pill but scissor-clipped to
/// the left band so insets stay positioned against the track rect
/// rather than a shrinking fill rect. Falls back to
/// `theme::track_bg()` / `theme::track_fill()` when the active
/// theme has no entry for either slot — a bare-bones third-party
/// theme still paints a usable slider.
pub fn draw( &self, canvas: &mut Canvas, rect: Rect, _focused: bool )
{
let pad = theme::THUMB_SIZE / 2.0;
let track_y = rect.y + (rect.height - theme::TRACK_H) / 2.0;
let track_start = rect.x + pad;
let track_end = rect.x + rect.width - pad;
let track_w = track_end - track_start;
let radius_bg = theme::TRACK_H / 2.0;
// Background track — full pill across the inner span.
let track_rect = Rect
{
x: track_start,
y: track_y,
width: track_w,
height: theme::TRACK_H,
};
if let Some( paint ) = self.track_paint.as_ref()
{
// Custom paint override (hue spectrum, custom gradient, …)
// short-circuits the theme-slot resolution. The
// active-side fill is suppressed below because a
// spectrum already encodes the position via colour.
canvas.fill_paint_rect( track_rect, paint, radius_bg );
}
else if let Some( ( surf, outer ) ) = crate::theme::resolve_surface( self.track_surface )
{
canvas.fill_surface
(
track_rect,
&surf.fill,
&outer,
&surf.inset_shadows,
radius_bg,
);
}
else
{
canvas.fill_rect( track_rect, theme::track_bg(), radius_bg );
}
// Filled portion. Paint the surface across the FULL track rect
// and clip the visible band to the left-aligned slice. This
// keeps inset shadows / glass rims anchored to the track pill
// instead of to a shrinking fill rect — the highlights don't
// shift as the user drags.
//
// Suppressed entirely when `track_paint` is set: a custom
// spectrum already conveys position through colour, and a
// solid accent band painted over part of it would obscure
// half the spectrum and read as a rendering bug.
let fill_w = track_w * self.value;
if self.track_paint.is_none() && fill_w > 0.5
{
let visible = Rect
{
x: track_start,
y: track_y,
width: fill_w,
height: theme::TRACK_H,
};
let saved_clip = canvas.clip_bounds();
let band = intersect_clip( &saved_clip, visible );
if !band.is_empty()
{
canvas.set_clip_rects( &band );
if let Some( ( surf, _ ) ) = crate::theme::resolve_surface( self.fill_surface )
{
canvas.fill_paint_rect( track_rect, &surf.fill, radius_bg );
// Drop insets with negative-Y offset — they live
// near the top of the track pill and would slice
// the fill at the active band edge. See
// `VSlider::draw` for the full rationale.
for inset in surf.inset_shadows.iter().filter( |s| s.offset[1] >= 0.0 )
{
canvas.fill_shadow_inset( track_rect, inset, radius_bg );
}
}
else
{
canvas.fill_rect( visible, theme::track_fill(), radius_bg );
}
canvas.set_clip_rects( &saved_clip );
}
}
// Thumb.
let thumb_cx = track_start + fill_w;
let thumb_cy = rect.y + rect.height / 2.0;
if self.accent_thumb
{
// Inner pill paints with the track's active fill surface
// so the thumb's centre always matches the line colour.
let outer_r = theme::ACCENT_THUMB_OUTER / 2.0;
let inner_size = ( theme::ACCENT_THUMB_OUTER - 2.0 * theme::ACCENT_THUMB_BORDER_W ).max( 0.0 );
let inner_r = inner_size / 2.0;
let outer_rect = Rect
{
x: thumb_cx - outer_r,
y: thumb_cy - outer_r,
width: theme::ACCENT_THUMB_OUTER,
height: theme::ACCENT_THUMB_OUTER,
};
let inner_rect = Rect
{
x: thumb_cx - inner_r,
y: thumb_cy - inner_r,
width: inner_size,
height: inner_size,
};
canvas.fill_rect( outer_rect, crate::theme::palette().bg, outer_r );
if let Some( ( surf, _ ) ) = crate::theme::resolve_surface( self.fill_surface )
{
canvas.fill_paint_rect( inner_rect, &surf.fill, inner_r );
}
else
{
canvas.fill_rect( inner_rect, crate::theme::palette().accent, inner_r );
}
}
else
{
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 );
}
}
pub( crate ) fn map_msg<U>( self, f: &super::MapFn<Msg, U> ) -> Slider<U>
where
U: Clone + 'static,
Msg: 'static,
{
// Wrap the existing Arc<dyn Fn(f32) -> Msg> in a fresh closure
// that pipes its result through the user's mapper. The original
// closure is captured by the new one through `Arc::clone` so the
// original Slider's storage stays valid.
let on_change = self.on_change.map( |old| -> Arc<dyn Fn( f32 ) -> U>
{
let mapper = Arc::clone( f );
Arc::new( move |v| ( *mapper )( ( *old )( v ) ) )
} );
Slider
{
value: self.value,
on_change,
track_surface: self.track_surface,
fill_surface: self.fill_surface,
accent_thumb: self.accent_thumb,
track_paint: self.track_paint,
}
}
}
/// Create a [`Slider`] with the given value (clamped to `[0.0, 1.0]`).
pub fn slider<Msg: Clone>( value: f32 ) -> Slider<Msg>
{
Slider::new( value )
}
fn surface_shadow_margin( surface: &str ) -> f32
{
crate::theme::resolve_surface( surface )
.map( |( _, shadows )|
{
shadows.iter()
.map( |s| s.offset[0].abs().max( s.offset[1].abs() ) + s.blur.max( 0.0 ) + s.spread.max( 0.0 ) + 1.0 )
.fold( 0.0, f32::max )
} )
.unwrap_or( 0.0 )
}
impl<Msg: Clone + 'static> From<Slider<Msg>> for Element<Msg>
{
fn from( s: Slider<Msg> ) -> Self
{
Element::Slider( s )
}
}