157 lines
4.2 KiB
Rust
157 lines
4.2 KiB
Rust
// SPDX-License-Identifier: LGPL-2.1-only
|
|
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
|
|
|
|
use crate::render::Canvas;
|
|
use crate::types::WidgetId;
|
|
use crate::widget::Element;
|
|
|
|
/// A vertically scrollable viewport that clips its child to its allocated rect.
|
|
///
|
|
/// The child can be any element — typically a [`Column`](crate::layout::column::Column)
|
|
/// for lists or a [`WrapGrid`](crate::layout::wrap_grid::WrapGrid) for icon grids.
|
|
///
|
|
/// Scroll offset is driven by touch/pointer drag gestures within the viewport.
|
|
/// Dragging inside a `Scroll` does **not** trigger the app-level swipe-up gesture.
|
|
///
|
|
/// ```rust,no_run
|
|
/// # use std::sync::Arc;
|
|
/// # use ltk::{ column, grid, icon_button, scroll, text, Element };
|
|
/// # #[ derive( Clone ) ] enum Msg {}
|
|
/// # fn _ex( rgba: Arc<Vec<u8>>, w: u32, h: u32 ) -> ( Element<Msg>, Element<Msg> ) {
|
|
/// // Scrollable list
|
|
/// let list = scroll( column().spacing( 8.0 ).push( text( "Item 1" ) ).push( text( "Item 2" ) ) );
|
|
///
|
|
/// // App-drawer style grid
|
|
/// let drawer = scroll( grid( 4 ).padding( 16.0 ).spacing( 12.0 ).push( icon_button( rgba, w, h ) ) );
|
|
/// # ( list.into(), drawer.into() )
|
|
/// # }
|
|
/// ```
|
|
pub struct Scroll<Msg: Clone>
|
|
{
|
|
/// The single child element drawn inside the scrollable viewport.
|
|
pub child: Box<Element<Msg>>,
|
|
/// Optional stable identifier — used as scroll state key.
|
|
pub id: Option<WidgetId>,
|
|
}
|
|
|
|
impl<Msg: Clone> Scroll<Msg>
|
|
{
|
|
/// Assign a stable identifier to this scroll widget.
|
|
pub fn id( mut self, id: WidgetId ) -> Self
|
|
{
|
|
self.id = Some( id );
|
|
self
|
|
}
|
|
|
|
/// Returns `(max_width, 0.0)` — the Scroll node claims all remaining space in
|
|
/// the parent layout, exactly like a [`Spacer`](crate::layout::spacer::Spacer).
|
|
/// The actual viewport height is determined at render time from the rect the
|
|
/// parent assigns.
|
|
pub fn preferred_size( &self, max_width: f32, _canvas: &Canvas ) -> (f32, f32)
|
|
{
|
|
( max_width, 0.0 )
|
|
}
|
|
|
|
/// No-op — rendering is handled entirely by `layout_and_draw` in `draw.rs`.
|
|
pub fn draw( &self ) {}
|
|
|
|
pub( crate ) fn map_msg<U>( self, f: &super::MapFn<Msg, U> ) -> Scroll<U>
|
|
where
|
|
U: Clone + 'static,
|
|
Msg: 'static,
|
|
{
|
|
Scroll
|
|
{
|
|
child: Box::new( self.child.map_arc( f ) ),
|
|
id: self.id,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<Msg: Clone + 'static> From<Scroll<Msg>> for Element<Msg>
|
|
{
|
|
fn from( s: Scroll<Msg> ) -> Self
|
|
{
|
|
Element::Scroll( s )
|
|
}
|
|
}
|
|
|
|
/// Compute the clamped scroll offset given raw offset, content height, and viewport height.
|
|
///
|
|
/// Extracted as a pure function so it can be unit-tested without a Wayland surface.
|
|
pub(crate) fn clamp_offset( offset: f32, content_h: f32, viewport_h: f32 ) -> f32
|
|
{
|
|
let max = (content_h - viewport_h).max( 0.0 );
|
|
offset.clamp( 0.0, max )
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests
|
|
{
|
|
use super::clamp_offset;
|
|
|
|
#[test]
|
|
fn offset_zero_when_content_fits()
|
|
{
|
|
// Content shorter than viewport — no scrolling possible
|
|
assert_eq!( clamp_offset( 50.0, 300.0, 500.0 ), 0.0 );
|
|
}
|
|
|
|
#[test]
|
|
fn offset_clamped_to_zero_when_negative()
|
|
{
|
|
assert_eq!( clamp_offset( -10.0, 600.0, 400.0 ), 0.0 );
|
|
}
|
|
|
|
#[test]
|
|
fn offset_clamped_to_max()
|
|
{
|
|
// max = 600 - 400 = 200; offset 999 → clamped to 200
|
|
assert_eq!( clamp_offset( 999.0, 600.0, 400.0 ), 200.0 );
|
|
}
|
|
|
|
#[test]
|
|
fn offset_within_range_unchanged()
|
|
{
|
|
// max = 600 - 400 = 200; offset 100 stays 100
|
|
assert_eq!( clamp_offset( 100.0, 600.0, 400.0 ), 100.0 );
|
|
}
|
|
|
|
#[test]
|
|
fn zero_offset_stays_zero()
|
|
{
|
|
assert_eq!( clamp_offset( 0.0, 600.0, 400.0 ), 0.0 );
|
|
}
|
|
|
|
#[test]
|
|
fn exact_max_offset_is_valid()
|
|
{
|
|
assert_eq!( clamp_offset( 200.0, 600.0, 400.0 ), 200.0 );
|
|
}
|
|
|
|
#[test]
|
|
fn content_equal_to_viewport_gives_zero_max()
|
|
{
|
|
// No overflow — max = 0
|
|
assert_eq!( clamp_offset( 10.0, 400.0, 400.0 ), 0.0 );
|
|
}
|
|
}
|
|
|
|
/// Create a scrollable viewport wrapping `child`.
|
|
///
|
|
/// The parent layout controls the viewport size by assigning a rect to this widget.
|
|
/// Content that overflows vertically is scrolled via drag gesture.
|
|
///
|
|
/// ```rust,no_run
|
|
/// # use ltk::{ column, scroll, text, Element };
|
|
/// # #[ derive( Clone ) ] enum Msg {}
|
|
/// # fn _ex() -> Element<Msg> {
|
|
/// scroll( column().push( text( "A" ) ).push( text( "B" ) ) )
|
|
/// .into()
|
|
/// # }
|
|
/// ```
|
|
pub fn scroll<Msg: Clone>( child: impl Into<Element<Msg>> ) -> Scroll<Msg>
|
|
{
|
|
Scroll { child: Box::new( child.into() ), id: None }
|
|
}
|