First commit. Version 0.1.0
This commit is contained in:
359
src/layout/column.rs
Normal file
359
src/layout/column.rs
Normal file
@@ -0,0 +1,359 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-only
|
||||
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
|
||||
|
||||
use crate::types::Rect;
|
||||
use crate::render::Canvas;
|
||||
use crate::widget::Element;
|
||||
|
||||
/// A vertical layout container.
|
||||
///
|
||||
/// Children are arranged top-to-bottom with optional spacing and padding.
|
||||
/// Spacers absorb remaining vertical space, enabling push-to-bottom layouts.
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use ltk::{ button, column, spacer, text, Element };
|
||||
/// # #[ derive( Clone ) ] enum Msg { Ok }
|
||||
/// # fn _ex() -> Element<Msg> {
|
||||
/// column()
|
||||
/// .padding( 24.0 )
|
||||
/// .spacing( 12.0 )
|
||||
/// .push( text( "Title" ) )
|
||||
/// .push( spacer() )
|
||||
/// .push( button( "OK" ).on_press( Msg::Ok ) )
|
||||
/// .into()
|
||||
/// # }
|
||||
/// ```
|
||||
pub struct Column<Msg: Clone>
|
||||
{
|
||||
pub children: Vec<Element<Msg>>,
|
||||
pub spacing: f32,
|
||||
pub padding: f32,
|
||||
pub align_center_x: bool,
|
||||
pub center_y: bool,
|
||||
pub max_width: Option<f32>,
|
||||
pub fit_content: bool,
|
||||
}
|
||||
|
||||
impl<Msg: Clone> Column<Msg>
|
||||
{
|
||||
pub fn new() -> Self
|
||||
{
|
||||
Self
|
||||
{
|
||||
children: Vec::new(),
|
||||
spacing: 8.0,
|
||||
padding: 16.0,
|
||||
align_center_x: true,
|
||||
center_y: false,
|
||||
max_width: None,
|
||||
fit_content: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Append a child widget or layout.
|
||||
pub fn push( mut self, e: impl Into<Element<Msg>> ) -> Self
|
||||
{
|
||||
self.children.push( e.into() );
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the vertical gap between children in pixels. Default: `8.0`.
|
||||
pub fn spacing( mut self, s: f32 ) -> Self
|
||||
{
|
||||
self.spacing = s;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the padding (all sides) in pixels. Default: `16.0`.
|
||||
pub fn padding( mut self, p: f32 ) -> Self
|
||||
{
|
||||
self.padding = p;
|
||||
self
|
||||
}
|
||||
|
||||
/// When `true` (default), children are centered horizontally.
|
||||
pub fn align_center_x( mut self, c: bool ) -> Self
|
||||
{
|
||||
self.align_center_x = c;
|
||||
self
|
||||
}
|
||||
|
||||
/// When `true`, center the content block vertically (only when no spacers are present).
|
||||
pub fn center_y( mut self, c: bool ) -> Self
|
||||
{
|
||||
self.center_y = c;
|
||||
self
|
||||
}
|
||||
|
||||
/// Limit the content width in pixels. The column still reports `max_width` as
|
||||
/// its preferred width so the parent allocates the full available rect.
|
||||
pub fn max_width( mut self, w: f32 ) -> Self
|
||||
{
|
||||
self.max_width = Some( w );
|
||||
self
|
||||
}
|
||||
|
||||
/// Report the intrinsic content width as preferred width instead of filling
|
||||
/// the available `max_width`. Use this when the column represents a card
|
||||
/// or widget meant to sit side-by-side with other children inside a
|
||||
/// [`Row`](crate::layout::row::Row) — without this flag, two columns in a
|
||||
/// row each claim the full row width and overflow their siblings.
|
||||
///
|
||||
/// The preferred width is computed as the max of children's preferred
|
||||
/// widths plus padding, capped by the external `max_width` the parent
|
||||
/// offers and by any `max_width` setting on the column itself.
|
||||
pub fn fit_content( mut self ) -> Self
|
||||
{
|
||||
self.fit_content = true;
|
||||
self
|
||||
}
|
||||
|
||||
fn inner_w( &self, available: f32 ) -> f32
|
||||
{
|
||||
let w = available - self.padding * 2.0;
|
||||
self.max_width.map( |m| w.min( m ) ).unwrap_or( w )
|
||||
}
|
||||
|
||||
fn content_h( &self, inner_w: f32, canvas: &Canvas ) -> f32
|
||||
{
|
||||
// Spacers contribute 0 to natural height; spacing still applies between all children.
|
||||
self.children.iter()
|
||||
.map( |c| match c
|
||||
{
|
||||
Element::Spacer( s ) => s.fixed_height.unwrap_or( 0.0 ),
|
||||
other => other.preferred_size( inner_w, canvas ).1,
|
||||
} )
|
||||
.sum::<f32>()
|
||||
+ self.spacing * (self.children.len().saturating_sub( 1 )) as f32
|
||||
}
|
||||
|
||||
/// Return the preferred `(width, height)` given available `max_width`.
|
||||
pub fn preferred_size( &self, max_width: f32, canvas: &Canvas ) -> (f32, f32)
|
||||
{
|
||||
let inner_w = self.inner_w( max_width );
|
||||
let total_h = self.content_h( inner_w, canvas ) + self.padding * 2.0;
|
||||
|
||||
let w = if self.fit_content
|
||||
{
|
||||
// "Filler" widgets (Spacer, Separator, Scroll, ProgressBar, Slider,
|
||||
// Toggle, TextEdit) all report `max_width` as their preferred width:
|
||||
// they stretch across whatever rect the parent allocates. Including
|
||||
// them when picking the intrinsic content width would claim
|
||||
// `max_width` and defeat the flag, so skip them — only content-sized
|
||||
// children (Text, Button, Image, nested fit-content Columns/Rows)
|
||||
// drive the natural width.
|
||||
let content_w = self.children.iter()
|
||||
.map( |c| match c
|
||||
{
|
||||
Element::Spacer( _ ) => 0.0,
|
||||
Element::Separator( _ ) => 0.0,
|
||||
Element::Scroll( _ ) => 0.0,
|
||||
Element::ProgressBar( _ ) => 0.0,
|
||||
Element::Slider( _ ) => 0.0,
|
||||
// TextEdit defaults to claiming `max_width`, but
|
||||
// a field built with `.fixed_width( w )` reports
|
||||
// a pinned natural size — let those through so a
|
||||
// numeric digit field inside a `fit_content`
|
||||
// stepper column can drive the column's width.
|
||||
Element::TextEdit( t ) => if t.fixed_width.is_some()
|
||||
{
|
||||
t.preferred_size( inner_w, canvas ).0
|
||||
} else { 0.0 },
|
||||
other => other.preferred_size( inner_w, canvas ).0,
|
||||
} )
|
||||
.fold( 0.0_f32, f32::max );
|
||||
( content_w + self.padding * 2.0 ).min( max_width )
|
||||
} else {
|
||||
max_width
|
||||
};
|
||||
|
||||
( w, total_h )
|
||||
}
|
||||
|
||||
pub fn draw( &self, _canvas: &mut Canvas, _rect: Rect, _focused: bool ) {}
|
||||
|
||||
/// Layout children within rect and return (rect, child_index) pairs.
|
||||
pub fn layout( &self, rect: Rect, canvas: &Canvas ) -> Vec<(Rect, usize)>
|
||||
{
|
||||
let inner_w = self.inner_w( rect.width );
|
||||
|
||||
// Flexible spacers and Scroll widgets claim remaining vertical space.
|
||||
// Fixed-height spacers behave like normal fixed-size children.
|
||||
let total_weight: u32 = self.children.iter()
|
||||
.map( |c| match c
|
||||
{
|
||||
Element::Spacer( s ) if s.fixed_height.is_none() => s.weight,
|
||||
Element::Scroll( _ ) => 1,
|
||||
_ => 0,
|
||||
} )
|
||||
.sum();
|
||||
|
||||
let fixed_h: f32 = self.children.iter()
|
||||
.map( |c|
|
||||
{
|
||||
if matches!( c, Element::Scroll( _ ) )
|
||||
{
|
||||
0.0
|
||||
} else if let Element::Spacer( s ) = c {
|
||||
s.fixed_height.unwrap_or( 0.0 )
|
||||
} else {
|
||||
c.preferred_size( inner_w, canvas ).1
|
||||
}
|
||||
} )
|
||||
.sum::<f32>()
|
||||
+ self.spacing * (self.children.len().saturating_sub( 1 )) as f32;
|
||||
|
||||
let avail_h = rect.height - self.padding * 2.0;
|
||||
let avail_spare = (avail_h - fixed_h).max( 0.0 );
|
||||
|
||||
// `center_y` only applies when there are no spacers.
|
||||
let start_y = if total_weight == 0 && self.center_y
|
||||
{
|
||||
rect.y + self.padding + avail_spare / 2.0
|
||||
} else {
|
||||
rect.y + self.padding
|
||||
};
|
||||
|
||||
let start_x = rect.x + (rect.width - inner_w) / 2.0;
|
||||
|
||||
let mut y = start_y;
|
||||
let mut result = Vec::new();
|
||||
for ( i, child ) in self.children.iter().enumerate()
|
||||
{
|
||||
let ( w, h ) = match child
|
||||
{
|
||||
Element::Spacer( s ) =>
|
||||
{
|
||||
let h = if let Some( fixed ) = s.fixed_height
|
||||
{
|
||||
fixed
|
||||
} else if total_weight > 0
|
||||
{
|
||||
avail_spare * s.weight as f32 / total_weight as f32
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
( inner_w, h )
|
||||
},
|
||||
Element::Scroll( _ ) =>
|
||||
{
|
||||
let h = if total_weight > 0
|
||||
{
|
||||
avail_spare / total_weight as f32
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
( inner_w, h )
|
||||
},
|
||||
other => other.preferred_size( inner_w, canvas ),
|
||||
};
|
||||
let x = if self.align_center_x && !matches!( child, Element::Spacer( _ ) )
|
||||
{
|
||||
start_x + (inner_w - w) / 2.0
|
||||
} else {
|
||||
start_x
|
||||
};
|
||||
result.push( ( Rect { x, y, width: w, height: h }, i ) );
|
||||
y += h + self.spacing;
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
pub( crate ) fn map_msg<U>( self, f: &crate::widget::MapFn<Msg, U> ) -> Column<U>
|
||||
where
|
||||
U: Clone + 'static,
|
||||
Msg: 'static,
|
||||
{
|
||||
Column
|
||||
{
|
||||
children: self.children.into_iter().map( |c| c.map_arc( f ) ).collect(),
|
||||
spacing: self.spacing,
|
||||
padding: self.padding,
|
||||
align_center_x: self.align_center_x,
|
||||
center_y: self.center_y,
|
||||
max_width: self.max_width,
|
||||
fit_content: self.fit_content,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an empty column layout.
|
||||
pub fn column<Msg: Clone>() -> Column<Msg>
|
||||
{
|
||||
Column::new()
|
||||
}
|
||||
|
||||
impl<Msg: Clone> Default for Column<Msg>
|
||||
{
|
||||
fn default() -> Self
|
||||
{
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[ cfg( test ) ]
|
||||
mod tests
|
||||
{
|
||||
use super::*;
|
||||
use crate::render::Canvas;
|
||||
|
||||
fn make_canvas() -> Canvas { Canvas::new( 800, 600 ) }
|
||||
|
||||
#[ test ]
|
||||
fn preferred_size_width_equals_max_width()
|
||||
{
|
||||
let canvas = make_canvas();
|
||||
let col = column::<()>().padding( 10.0 );
|
||||
let ( w, _ ) = col.preferred_size( 200.0, &canvas );
|
||||
assert_eq!( w, 200.0 );
|
||||
}
|
||||
|
||||
#[ test ]
|
||||
fn empty_column_height_is_two_paddings()
|
||||
{
|
||||
let canvas = make_canvas();
|
||||
let col = column::<()>().padding( 10.0 );
|
||||
let ( _, h ) = col.preferred_size( 200.0, &canvas );
|
||||
assert_eq!( h, 20.0 );
|
||||
}
|
||||
|
||||
#[ test ]
|
||||
fn max_width_caps_inner_w_not_preferred_w()
|
||||
{
|
||||
let canvas = make_canvas();
|
||||
// preferred_size always returns available max_width; max_width caps inner layout only
|
||||
let col = column::<()>().padding( 0.0 ).max_width( 100.0 );
|
||||
let ( w, _ ) = col.preferred_size( 200.0, &canvas );
|
||||
assert_eq!( w, 200.0 );
|
||||
}
|
||||
|
||||
#[ test ]
|
||||
fn inner_w_respects_padding_and_max_width()
|
||||
{
|
||||
let col = column::<()>().padding( 20.0 ).max_width( 100.0 );
|
||||
// available = 200, minus padding*2 = 160, capped at max_width = 100
|
||||
assert_eq!( col.inner_w( 200.0 ), 100.0 );
|
||||
}
|
||||
|
||||
#[ test ]
|
||||
fn inner_w_without_max_width_subtracts_padding()
|
||||
{
|
||||
let col = column::<()>().padding( 10.0 );
|
||||
assert_eq!( col.inner_w( 200.0 ), 180.0 );
|
||||
}
|
||||
|
||||
#[ test ]
|
||||
fn spacing_between_children_accumulates()
|
||||
{
|
||||
let canvas = make_canvas();
|
||||
// Three zero-height spacers, two 8 px gaps between them = 16.
|
||||
let col = column::<()>()
|
||||
.padding( 0.0 )
|
||||
.spacing( 8.0 )
|
||||
.push( crate::spacer() )
|
||||
.push( crate::spacer() )
|
||||
.push( crate::spacer() );
|
||||
let ( _, h ) = col.preferred_size( 100.0, &canvas );
|
||||
assert_eq!( h, 16.0 );
|
||||
}
|
||||
}
|
||||
47
src/layout/mod.rs
Normal file
47
src/layout/mod.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-only
|
||||
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
|
||||
|
||||
//! Layouts — composable arrangers for [`Element`](crate::Element) trees.
|
||||
//!
|
||||
//! Layouts decide *where* their children sit; they don't paint anything of
|
||||
//! their own. Each layout exposes a free constructor (`column()`, `row()`,
|
||||
//! `stack()`, `grid(N)`, `spacer()`) and a builder-style API for spacing,
|
||||
//! padding, alignment and sizing. Layouts and [widgets](crate::widget)
|
||||
//! share the same [`Element<Msg>`](crate::Element) tree — anything that
|
||||
//! converts into an `Element` can be pushed into any layout.
|
||||
//!
|
||||
//! ## What's available
|
||||
//!
|
||||
//! * **Flow** — [`column::Column`] (top-to-bottom),
|
||||
//! [`row::Row`] (left-to-right). Both honour `padding`, `spacing`,
|
||||
//! `max_width`, `align_center_*` and inline `Spacer` distribution.
|
||||
//! * **Overlay** — [`stack::Stack`] for FrameLayout-style layering with
|
||||
//! per-child [`HAlign`](stack::HAlign) / [`VAlign`](stack::VAlign) and
|
||||
//! margin / pixel-translation overrides. Useful for foreground HUD on
|
||||
//! top of a background image, or a floating action button anchored to
|
||||
//! the bottom-right.
|
||||
//! * **Grid** — [`wrap_grid::WrapGrid`] for fixed-column-count grids that
|
||||
//! wrap their children into rows (icon launchers, photo galleries).
|
||||
//! * **Filler** — [`spacer::Spacer`], an invisible child that absorbs
|
||||
//! leftover space along the parent's main axis. Pair with
|
||||
//! [`flex::Flex`](crate::Flex) when the filler is non-trivial (a card
|
||||
//! that should stretch a row).
|
||||
//!
|
||||
//! Most layouts default to "fill the parent's available rect" and only
|
||||
//! shrink to their content with `fit_content()`.
|
||||
//!
|
||||
//! ## Sizing model
|
||||
//!
|
||||
//! Layouts implement `preferred_size( max_width, &Canvas ) -> ( w, h )`
|
||||
//! the same way widgets do. The convention is: the layout claims the
|
||||
//! parent-supplied `max_width` (or the explicit `max_width(...)` setting)
|
||||
//! and reports the height needed for its children. Spacers and
|
||||
//! `Scroll`/`Flex` declare zero intrinsic main-axis size and absorb the
|
||||
//! leftover space the parent has after fixed-size siblings have been laid
|
||||
//! out.
|
||||
|
||||
pub mod column;
|
||||
pub mod row;
|
||||
pub mod spacer;
|
||||
pub mod stack;
|
||||
pub mod wrap_grid;
|
||||
299
src/layout/row.rs
Normal file
299
src/layout/row.rs
Normal file
@@ -0,0 +1,299 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-only
|
||||
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
|
||||
|
||||
use crate::types::Rect;
|
||||
use crate::render::Canvas;
|
||||
use crate::widget::Element;
|
||||
|
||||
/// A horizontal layout container.
|
||||
///
|
||||
/// Children are arranged left-to-right. Use [`Row::align_right`] to
|
||||
/// push the content block to the right edge of the available width.
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use std::sync::Arc;
|
||||
/// # use ltk::{ icon_button, row, Element };
|
||||
/// # #[ derive( Clone ) ] enum Msg { A, B }
|
||||
/// # fn _ex( a_rgba: Arc<Vec<u8>>, b_rgba: Arc<Vec<u8>>, w: u32, h: u32 ) -> Element<Msg> {
|
||||
/// row()
|
||||
/// .spacing( 16.0 )
|
||||
/// .align_right()
|
||||
/// .push( icon_button( a_rgba, w, h ).on_press( Msg::A ) )
|
||||
/// .push( icon_button( b_rgba, w, h ).on_press( Msg::B ) )
|
||||
/// .into()
|
||||
/// # }
|
||||
/// ```
|
||||
pub struct Row<Msg: Clone>
|
||||
{
|
||||
pub children: Vec<Element<Msg>>,
|
||||
pub spacing: f32,
|
||||
pub padding: f32,
|
||||
pub align_right: bool,
|
||||
}
|
||||
|
||||
impl<Msg: Clone> Row<Msg>
|
||||
{
|
||||
pub fn new() -> Self
|
||||
{
|
||||
Self { children: Vec::new(), spacing: 8.0, padding: 0.0, align_right: false }
|
||||
}
|
||||
|
||||
/// Append a child widget or layout.
|
||||
pub fn push( mut self, e: impl Into<Element<Msg>> ) -> Self
|
||||
{
|
||||
self.children.push( e.into() );
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the horizontal gap between children in pixels. Default: `8.0`.
|
||||
pub fn spacing( mut self, s: f32 ) -> Self
|
||||
{
|
||||
self.spacing = s;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the padding (all sides) in pixels. Default: `0.0`.
|
||||
pub fn padding( mut self, p: f32 ) -> Self
|
||||
{
|
||||
self.padding = p;
|
||||
self
|
||||
}
|
||||
|
||||
/// Push the content block to the right edge of the available width.
|
||||
pub fn align_right( mut self ) -> Self
|
||||
{
|
||||
self.align_right = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Return the preferred `(width, height)` given available `max_width`.
|
||||
pub fn preferred_size( &self, max_width: f32, canvas: &Canvas ) -> (f32, f32)
|
||||
{
|
||||
// Width contribution of every fixed (non-flex, non-flex-spacer) child.
|
||||
// Used to compute the residual width that wrap-style children will
|
||||
// actually render in, so their reported height matches the layout.
|
||||
let inner_w = ( max_width - self.padding * 2.0 ).max( 0.0 );
|
||||
let gaps = self.spacing * self.children.len().saturating_sub( 1 ) as f32;
|
||||
let fixed_w: f32 = self.children.iter()
|
||||
.filter( |c| match c
|
||||
{
|
||||
Element::Flex( _ ) => false,
|
||||
Element::Spacer( s ) => s.fixed_width.is_some(),
|
||||
_ => true,
|
||||
} )
|
||||
.map( |c| c.preferred_size( max_width, canvas ).0 )
|
||||
.sum();
|
||||
let residual = ( inner_w - fixed_w - gaps ).max( 0.0 );
|
||||
|
||||
let max_h: f32 = self.children.iter()
|
||||
.map( |c| match c
|
||||
{
|
||||
Element::Flex( _ ) => c.preferred_size( residual, canvas ).1,
|
||||
Element::Spacer( _ ) => c.preferred_size( max_width, canvas ).1,
|
||||
_ => c.preferred_size( max_width, canvas ).1,
|
||||
} )
|
||||
.fold( 0.0_f32, f32::max );
|
||||
|
||||
// `align_right` and any flex / weight-only spacer child make the row
|
||||
// claim the full `max_width`: in those cases the row's rendered width
|
||||
// comes from leftover distribution (or the right-edge anchor), not
|
||||
// from the sum of children's preferred widths. Reporting only the
|
||||
// natural sum would leave the parent allocating a too-narrow rect and
|
||||
// the flex children would collapse to 0.
|
||||
let has_flex = self.children.iter().any( |c| match c
|
||||
{
|
||||
Element::Flex( _ ) => true,
|
||||
Element::Spacer( s ) => s.fixed_width.is_none(),
|
||||
_ => false,
|
||||
} );
|
||||
|
||||
let w = if self.align_right || has_flex
|
||||
{
|
||||
max_width
|
||||
} else {
|
||||
let total_w: f32 = self.children.iter()
|
||||
.map( |c| c.preferred_size( max_width, canvas ).0 )
|
||||
.sum::<f32>()
|
||||
+ gaps
|
||||
+ self.padding * 2.0;
|
||||
total_w.min( max_width )
|
||||
};
|
||||
|
||||
( w, max_h + self.padding * 2.0 )
|
||||
}
|
||||
|
||||
pub fn draw( &self, _canvas: &mut Canvas, _rect: Rect, _focused: bool ) {}
|
||||
|
||||
/// Layout children within rect and return `(rect, child_index)` pairs.
|
||||
///
|
||||
/// Flexible [`Spacer`](crate::layout::spacer::Spacer) children claim the
|
||||
/// leftover horizontal space: non-spacer widgets keep their preferred
|
||||
/// width, the remaining width (after subtracting spacing + padding) is
|
||||
/// distributed between spacers in proportion to their `weight`. When no
|
||||
/// spacers are present the cluster is centered (or right-aligned via
|
||||
/// [`Row::align_right`]).
|
||||
pub fn layout( &self, rect: Rect, canvas: &Canvas ) -> Vec<(Rect, usize)>
|
||||
{
|
||||
let inner_h = rect.height - self.padding * 2.0;
|
||||
|
||||
// Spacers and `Flex` wrappers report 0 width here; their real width
|
||||
// comes from the flex distribution below.
|
||||
let sizes: Vec<(f32, f32)> = self.children.iter()
|
||||
.map( |c| c.preferred_size( rect.width, canvas ) )
|
||||
.collect();
|
||||
|
||||
let gaps = self.spacing * self.children.len().saturating_sub( 1 ) as f32;
|
||||
let fixed_w: f32 = self.children.iter().zip( sizes.iter() )
|
||||
.filter( |( c, _ )| match c
|
||||
{
|
||||
// Pure flex children (`Flex` and weight-only `Spacer`) take
|
||||
// width from the leftover pool; everything else, including
|
||||
// `Spacer::width(...)`-pinned spacers, contributes to the
|
||||
// fixed-width tally.
|
||||
Element::Flex( _ ) => false,
|
||||
Element::Spacer( s ) => s.fixed_width.is_some(),
|
||||
_ => true,
|
||||
} )
|
||||
.map( |( _, ( w, _ ) )| *w )
|
||||
.sum();
|
||||
|
||||
let total_weight: u32 = self.children.iter()
|
||||
.filter_map( |c| match c {
|
||||
Element::Spacer( s ) if s.fixed_width.is_none() => Some( s.weight ),
|
||||
Element::Flex( f ) => Some( f.weight ),
|
||||
_ => None,
|
||||
} )
|
||||
.sum();
|
||||
|
||||
let inner_w = ( rect.width - self.padding * 2.0 ).max( 0.0 );
|
||||
let leftover = ( inner_w - fixed_w - gaps ).max( 0.0 );
|
||||
let has_spacers = total_weight > 0;
|
||||
|
||||
let ( start_x, flex_unit ) = if has_spacers
|
||||
{
|
||||
// Spacers and `Flex` wrappers claim the leftover; the cluster
|
||||
// sits flush to the left edge of the inner rect.
|
||||
( rect.x + self.padding, leftover / total_weight as f32 )
|
||||
}
|
||||
else if self.align_right
|
||||
{
|
||||
( rect.x + rect.width - (fixed_w + gaps) - self.padding, 0.0 )
|
||||
}
|
||||
else
|
||||
{
|
||||
( rect.x + (rect.width - fixed_w - gaps) / 2.0, 0.0 )
|
||||
};
|
||||
|
||||
let mut x = start_x;
|
||||
let mut result = Vec::with_capacity( self.children.len() );
|
||||
for ( i, ( (w, h), child) ) in sizes.into_iter().zip( self.children.iter() ).enumerate()
|
||||
{
|
||||
let width = match child
|
||||
{
|
||||
Element::Spacer( s ) => match s.fixed_width
|
||||
{
|
||||
Some( fw ) => fw,
|
||||
None => flex_unit * s.weight as f32,
|
||||
},
|
||||
Element::Flex( f ) => flex_unit * f.weight as f32,
|
||||
_ => w,
|
||||
};
|
||||
let y = rect.y + self.padding + (inner_h - h) / 2.0;
|
||||
result.push( ( Rect { x, y, width, height: h }, i ) );
|
||||
x += width + self.spacing;
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
pub( crate ) fn map_msg<U>( self, f: &crate::widget::MapFn<Msg, U> ) -> Row<U>
|
||||
where
|
||||
U: Clone + 'static,
|
||||
Msg: 'static,
|
||||
{
|
||||
Row
|
||||
{
|
||||
children: self.children.into_iter().map( |c| c.map_arc( f ) ).collect(),
|
||||
spacing: self.spacing,
|
||||
padding: self.padding,
|
||||
align_right: self.align_right,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an empty row layout.
|
||||
pub fn row<Msg: Clone>() -> Row<Msg>
|
||||
{
|
||||
Row::new()
|
||||
}
|
||||
|
||||
impl<Msg: Clone> Default for Row<Msg>
|
||||
{
|
||||
fn default() -> Self
|
||||
{
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[ cfg( test ) ]
|
||||
mod tests
|
||||
{
|
||||
use super::*;
|
||||
use crate::render::Canvas;
|
||||
use crate::types::Rect;
|
||||
|
||||
fn make_canvas() -> Canvas { Canvas::new( 800, 600 ) }
|
||||
|
||||
#[ test ]
|
||||
fn align_right_returns_full_max_width()
|
||||
{
|
||||
let canvas = make_canvas();
|
||||
let r = row::<()>().align_right();
|
||||
let ( w, _ ) = r.preferred_size( 500.0, &canvas );
|
||||
assert_eq!( w, 500.0 );
|
||||
}
|
||||
|
||||
#[ test ]
|
||||
fn align_right_true_regardless_of_children()
|
||||
{
|
||||
let canvas = make_canvas();
|
||||
let r = row::<()>().align_right().spacing( 999.0 );
|
||||
let ( w, _ ) = r.preferred_size( 300.0, &canvas );
|
||||
assert_eq!( w, 300.0 );
|
||||
}
|
||||
|
||||
#[ test ]
|
||||
fn centered_empty_row_returns_zero_width()
|
||||
{
|
||||
let canvas = make_canvas();
|
||||
let r = row::<()>();
|
||||
let ( w, _ ) = r.preferred_size( 500.0, &canvas );
|
||||
assert_eq!( w, 0.0 );
|
||||
}
|
||||
|
||||
#[ test ]
|
||||
fn centered_empty_row_returns_zero_height()
|
||||
{
|
||||
let canvas = make_canvas();
|
||||
let r = row::<()>().padding( 0.0 );
|
||||
let ( _, h ) = r.preferred_size( 500.0, &canvas );
|
||||
assert_eq!( h, 0.0 );
|
||||
}
|
||||
|
||||
#[ test ]
|
||||
fn padding_adds_to_height()
|
||||
{
|
||||
let canvas = make_canvas();
|
||||
let r = row::<()>().padding( 8.0 );
|
||||
let ( _, h ) = r.preferred_size( 500.0, &canvas );
|
||||
assert_eq!( h, 16.0 );
|
||||
}
|
||||
|
||||
#[ test ]
|
||||
fn layout_of_empty_row_is_empty()
|
||||
{
|
||||
let canvas = make_canvas();
|
||||
let r = row::<()>().align_right();
|
||||
let rect = Rect { x: 0., y: 0., width: 400., height: 48. };
|
||||
assert!( r.layout( rect, &canvas ).is_empty() );
|
||||
}
|
||||
}
|
||||
143
src/layout/spacer.rs
Normal file
143
src/layout/spacer.rs
Normal file
@@ -0,0 +1,143 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-only
|
||||
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
|
||||
|
||||
use crate::widget::Element;
|
||||
|
||||
/// A flexible, invisible spacer that expands to fill available space.
|
||||
///
|
||||
/// The optional `weight` controls how much of the remaining space this spacer
|
||||
/// claims relative to other spacers in the same layout. A spacer with `weight = 2`
|
||||
/// takes twice as much space as one with `weight = 1`.
|
||||
///
|
||||
/// Place a `Spacer` between two widgets inside a [`Column`](crate::layout::column::Column)
|
||||
/// or [`Row`](crate::layout::row::Row) to push them apart:
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use ltk::{ column, spacer, text, Element };
|
||||
/// # #[ derive( Clone ) ] enum Msg {}
|
||||
/// # fn _ex() -> Element<Msg> {
|
||||
/// column()
|
||||
/// .push( text( "Top" ) )
|
||||
/// .push( spacer() ) // pushes "Bottom" to the bottom
|
||||
/// .push( text( "Bottom" ) )
|
||||
/// .into()
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// Use [`.weight(n)`](Spacer::weight) to replace several consecutive spacers:
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use ltk::{ column, spacer, Column };
|
||||
/// # #[ derive( Clone ) ] enum Msg {}
|
||||
/// # fn _ex() {
|
||||
/// // These two are equivalent:
|
||||
/// let _: Column<Msg> = column().push( spacer().weight( 3 ) );
|
||||
/// let _: Column<Msg> = column().push( spacer() ).push( spacer() ).push( spacer() );
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// Use [`.height(px)`](Spacer::height) to create a fixed-size vertical spacer:
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use ltk::{ column, spacer, text, Element };
|
||||
/// # #[ derive( Clone ) ] enum Msg {}
|
||||
/// # fn _ex() -> Element<Msg> {
|
||||
/// column()
|
||||
/// .push( text( "Header" ) )
|
||||
/// .push( spacer().height( 20.0 ) ) // Exactly 20px gap
|
||||
/// .push( text( "Content" ) )
|
||||
/// .into()
|
||||
/// # }
|
||||
/// ```
|
||||
pub struct Spacer
|
||||
{
|
||||
/// Relative weight of this spacer (default 1).
|
||||
pub weight: u32,
|
||||
/// Fixed height in pixels (overrides flexible behavior in a column).
|
||||
pub fixed_height: Option<f32>,
|
||||
/// Fixed width in pixels (overrides flexible behavior in a row).
|
||||
pub fixed_width: Option<f32>,
|
||||
}
|
||||
|
||||
impl Spacer
|
||||
{
|
||||
/// Set the relative weight of this spacer (default 1).
|
||||
pub fn weight( mut self, w: u32 ) -> Self
|
||||
{
|
||||
self.weight = w;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set a fixed height for this spacer in pixels.
|
||||
/// When set, the spacer will occupy exactly this much vertical space
|
||||
/// instead of expanding flexibly.
|
||||
pub fn height( mut self, h: f32 ) -> Self
|
||||
{
|
||||
self.fixed_height = Some( h );
|
||||
self
|
||||
}
|
||||
|
||||
/// Set a fixed width for this spacer in pixels.
|
||||
/// When set, the spacer occupies exactly this much horizontal space
|
||||
/// inside a [`Row`](crate::layout::row::Row) instead of expanding
|
||||
/// flexibly. Mirrors [`Self::height`] for the horizontal axis — useful
|
||||
/// to reserve a precise visual margin while a sibling
|
||||
/// [`Flex`](crate::Flex) claims the remaining width.
|
||||
pub fn width( mut self, w: f32 ) -> Self
|
||||
{
|
||||
self.fixed_width = Some( w );
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns `( fixed_width, fixed_height )`, falling back to `0.0` on the
|
||||
/// axes that were not pinned. The parent layout distributes leftover
|
||||
/// along its main axis among the still-flexible spacers and `Flex`
|
||||
/// wrappers, weighted by `weight`.
|
||||
pub fn preferred_size( &self ) -> (f32, f32)
|
||||
{
|
||||
( self.fixed_width.unwrap_or( 0.0 ), self.fixed_height.unwrap_or( 0.0 ) )
|
||||
}
|
||||
|
||||
/// No-op — spacers are invisible.
|
||||
pub fn draw( &self ) {}
|
||||
}
|
||||
|
||||
impl<Msg: Clone + 'static> From<Spacer> for Element<Msg>
|
||||
{
|
||||
fn from( s: Spacer ) -> Self
|
||||
{
|
||||
Element::Spacer( s )
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a flexible spacer with weight 1.
|
||||
///
|
||||
/// Call [`.weight(n)`](Spacer::weight) to set a relative weight greater than 1:
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use ltk::{ column, spacer, Column };
|
||||
/// # #[ derive( Clone ) ] enum Msg {}
|
||||
/// # fn _ex() {
|
||||
/// // These two are equivalent:
|
||||
/// let _: Column<Msg> = column().push( spacer().weight( 3 ) );
|
||||
/// let _: Column<Msg> = column().push( spacer() ).push( spacer() ).push( spacer() );
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// Call [`.height(px)`](Spacer::height) to create a fixed-size vertical gap:
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use ltk::{ column, spacer, text, Element };
|
||||
/// # #[ derive( Clone ) ] enum Msg {}
|
||||
/// # fn _ex() -> Element<Msg> {
|
||||
/// column()
|
||||
/// .push( text( "First" ) )
|
||||
/// .push( spacer().height( 24.0 ) ) // Fixed 24px gap
|
||||
/// .push( text( "Second" ) )
|
||||
/// .into()
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn spacer() -> Spacer
|
||||
{
|
||||
Spacer { weight: 1, fixed_height: None, fixed_width: None }
|
||||
}
|
||||
182
src/layout/stack.rs
Normal file
182
src/layout/stack.rs
Normal file
@@ -0,0 +1,182 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-only
|
||||
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
|
||||
|
||||
use crate::types::Rect;
|
||||
use crate::render::Canvas;
|
||||
use crate::widget::Element;
|
||||
|
||||
/// Horizontal alignment of a child within a [`Stack`] rect.
|
||||
#[ derive( Debug, Clone, Copy, PartialEq ) ]
|
||||
pub enum HAlign
|
||||
{
|
||||
/// Align to the left edge.
|
||||
Start,
|
||||
/// Center horizontally.
|
||||
Center,
|
||||
/// Align to the right edge.
|
||||
End,
|
||||
/// Stretch to fill the full width.
|
||||
Fill,
|
||||
}
|
||||
|
||||
/// Vertical alignment of a child within a [`Stack`] rect.
|
||||
#[ derive( Debug, Clone, Copy, PartialEq ) ]
|
||||
pub enum VAlign
|
||||
{
|
||||
/// Align to the top edge.
|
||||
Top,
|
||||
/// Center vertically.
|
||||
Center,
|
||||
/// Align to the bottom edge.
|
||||
Bottom,
|
||||
/// Stretch to fill the full height.
|
||||
Fill,
|
||||
}
|
||||
|
||||
/// A layout that draws all its children stacked on top of each other.
|
||||
/// Each child can be positioned within the Stack rect via [`HAlign`]/[`VAlign`].
|
||||
///
|
||||
/// Useful for overlaying a foreground widget on top of a background image:
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use std::sync::Arc;
|
||||
/// # use ltk::{ column, img_widget, stack, text, Element, HAlign, VAlign };
|
||||
/// # #[ derive( Clone ) ] enum Msg {}
|
||||
/// # fn _ex( bg_rgba: Arc<Vec<u8>>, w: u32, h: u32 ) -> Element<Msg> {
|
||||
/// stack()
|
||||
/// .push( img_widget( bg_rgba, w, h ) )
|
||||
/// .push_aligned( column().push( text( "Bottom right" ) ), HAlign::End, VAlign::Bottom )
|
||||
/// .into()
|
||||
/// # }
|
||||
/// ```
|
||||
pub struct Stack<Msg: Clone>
|
||||
{
|
||||
/// Children with their alignment, margin, and extra `(x, y)` translation
|
||||
/// applied after alignment. Drawn in order — last child is on top.
|
||||
pub children: Vec<( Element<Msg>, HAlign, VAlign, f32, f32, f32 )>,
|
||||
}
|
||||
|
||||
impl<Msg: Clone> Stack<Msg>
|
||||
{
|
||||
/// Create an empty stack.
|
||||
pub fn new() -> Self
|
||||
{
|
||||
Self { children: Vec::new() }
|
||||
}
|
||||
|
||||
/// Append a child that fills the entire Stack rect (Android FrameLayout default).
|
||||
pub fn push( self, e: impl Into<Element<Msg>> ) -> Self
|
||||
{
|
||||
self.push_aligned_margin( e, HAlign::Fill, VAlign::Fill, 0.0 )
|
||||
}
|
||||
|
||||
/// Append a child with explicit horizontal and vertical alignment.
|
||||
pub fn push_aligned(
|
||||
self,
|
||||
e: impl Into<Element<Msg>>,
|
||||
h_align: HAlign,
|
||||
v_align: VAlign,
|
||||
) -> Self
|
||||
{
|
||||
self.push_aligned_margin( e, h_align, v_align, 0.0 )
|
||||
}
|
||||
|
||||
/// Append a child with alignment and a uniform margin (inset from the Stack edges).
|
||||
pub fn push_aligned_margin(
|
||||
mut self,
|
||||
e: impl Into<Element<Msg>>,
|
||||
h_align: HAlign,
|
||||
v_align: VAlign,
|
||||
margin: f32,
|
||||
) -> Self
|
||||
{
|
||||
self.children.push( ( e.into(), h_align, v_align, margin, 0.0, 0.0 ) );
|
||||
self
|
||||
}
|
||||
|
||||
/// Append a child with alignment plus an extra `(x, y)` translation in
|
||||
/// logical pixels. Useful when a child needs to shift outside the normal
|
||||
/// alignment grid without giving up the margin or alignment shorthand.
|
||||
/// Positive `x` / `y` move the child right / down.
|
||||
pub fn push_translated(
|
||||
mut self,
|
||||
e: impl Into<Element<Msg>>,
|
||||
h_align: HAlign,
|
||||
v_align: VAlign,
|
||||
offset_x: f32,
|
||||
offset_y: f32,
|
||||
) -> Self
|
||||
{
|
||||
self.children.push( ( e.into(), h_align, v_align, 0.0, offset_x, offset_y ) );
|
||||
self
|
||||
}
|
||||
|
||||
/// Return the preferred `(width, height)` — the maximum height among children.
|
||||
pub fn preferred_size( &self, max_width: f32, canvas: &Canvas ) -> (f32, f32)
|
||||
{
|
||||
let max_h = self.children.iter()
|
||||
.map( |( c, _, _, _, _, _ )| c.preferred_size( max_width, canvas ).1 )
|
||||
.fold( 0.0_f32, f32::max );
|
||||
( max_width, max_h )
|
||||
}
|
||||
|
||||
/// Return `(rect, child_index)` pairs, computing each child's rect from its alignment.
|
||||
pub fn layout( &self, rect: Rect, canvas: &Canvas ) -> Vec<(Rect, usize)>
|
||||
{
|
||||
self.children.iter().enumerate().map( |( i, ( child, h_align, v_align, margin, ox, oy ) )|
|
||||
{
|
||||
let inner_w = ( rect.width - margin * 2.0 ).max( 0.0 );
|
||||
let inner_h = ( rect.height - margin * 2.0 ).max( 0.0 );
|
||||
let ( pref_w, pref_h ) = child.preferred_size( inner_w, canvas );
|
||||
|
||||
let ( x, width ) = match h_align
|
||||
{
|
||||
HAlign::Start => ( rect.x + margin, pref_w ),
|
||||
HAlign::Center => ( rect.x + ( rect.width - pref_w ) / 2.0, pref_w ),
|
||||
HAlign::End => ( rect.x + rect.width - pref_w - margin, pref_w ),
|
||||
HAlign::Fill => ( rect.x + margin, inner_w ),
|
||||
};
|
||||
|
||||
let ( y, height ) = match v_align
|
||||
{
|
||||
VAlign::Top => ( rect.y + margin, pref_h ),
|
||||
VAlign::Center => ( rect.y + ( rect.height - pref_h ) / 2.0, pref_h ),
|
||||
VAlign::Bottom => ( rect.y + rect.height - pref_h - margin, pref_h ),
|
||||
VAlign::Fill => ( rect.y + margin, inner_h ),
|
||||
};
|
||||
|
||||
( Rect { x: x + ox, y: y + oy, width, height }, i )
|
||||
} ).collect()
|
||||
}
|
||||
|
||||
/// No-op — children are drawn directly by the event loop during layout.
|
||||
pub fn draw( &self, _canvas: &mut Canvas, _rect: Rect, _focused: bool ) {}
|
||||
|
||||
pub( crate ) fn map_msg<U>( self, f: &crate::widget::MapFn<Msg, U> ) -> Stack<U>
|
||||
where
|
||||
U: Clone + 'static,
|
||||
Msg: 'static,
|
||||
{
|
||||
Stack
|
||||
{
|
||||
children: self.children.into_iter()
|
||||
.map( |( child, ha, va, margin, ox, oy )|
|
||||
( child.map_arc( f ), ha, va, margin, ox, oy ) )
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Msg: Clone> Default for Stack<Msg>
|
||||
{
|
||||
fn default() -> Self
|
||||
{
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an empty [`Stack`].
|
||||
pub fn stack<Msg: Clone>() -> Stack<Msg>
|
||||
{
|
||||
Stack::new()
|
||||
}
|
||||
347
src/layout/wrap_grid.rs
Normal file
347
src/layout/wrap_grid.rs
Normal file
@@ -0,0 +1,347 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-only
|
||||
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
|
||||
|
||||
use crate::render::Canvas;
|
||||
use crate::types::Rect;
|
||||
use crate::widget::Element;
|
||||
|
||||
/// A grid layout that wraps children into rows of a fixed column count.
|
||||
///
|
||||
/// All cells in a row share the same height (the tallest item in that row).
|
||||
/// Column widths are equal, dividing the available width minus padding and spacing.
|
||||
///
|
||||
/// Designed for app-drawer style layouts — combine with [`scroll()`](crate::widget::scroll::scroll)
|
||||
/// for vertically scrollable grids:
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use std::sync::Arc;
|
||||
/// # use ltk::{ grid, icon_button, scroll, Element };
|
||||
/// # #[ derive( Clone ) ] enum Msg { Open( usize ) }
|
||||
/// # fn _ex( data: Arc<Vec<u8>>, w: u32, h: u32 ) -> Element<Msg> {
|
||||
/// scroll(
|
||||
/// grid( 4 )
|
||||
/// .padding( 16.0 )
|
||||
/// .spacing( 12.0 )
|
||||
/// .push( icon_button( data.clone(), w, h ).on_press( Msg::Open( 0 ) ) )
|
||||
/// .push( icon_button( data, w, h ).on_press( Msg::Open( 1 ) ) )
|
||||
/// // ...
|
||||
/// )
|
||||
/// .into()
|
||||
/// # }
|
||||
/// ```
|
||||
pub struct WrapGrid<Msg: Clone>
|
||||
{
|
||||
/// Child widgets laid out in row-major order.
|
||||
pub children: Vec<Element<Msg>>,
|
||||
/// Number of columns per row.
|
||||
pub columns: usize,
|
||||
/// Horizontal gap between cells (pixels).
|
||||
pub spacing_x: f32,
|
||||
/// Vertical gap between rows (pixels).
|
||||
pub spacing_y: f32,
|
||||
/// Padding on all sides (pixels).
|
||||
pub padding: f32,
|
||||
}
|
||||
|
||||
impl<Msg: Clone> WrapGrid<Msg>
|
||||
{
|
||||
/// Append a child widget to the grid.
|
||||
pub fn push( mut self, child: impl Into<Element<Msg>> ) -> Self
|
||||
{
|
||||
self.children.push( child.into() );
|
||||
self
|
||||
}
|
||||
|
||||
/// Set both horizontal and vertical gap between cells (default 8.0).
|
||||
pub fn spacing( mut self, s: f32 ) -> Self
|
||||
{
|
||||
self.spacing_x = s;
|
||||
self.spacing_y = s;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set only the horizontal gap between cells; leaves vertical spacing untouched.
|
||||
pub fn spacing_x( mut self, s: f32 ) -> Self
|
||||
{
|
||||
self.spacing_x = s;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set only the vertical gap between rows; leaves horizontal spacing untouched.
|
||||
pub fn spacing_y( mut self, s: f32 ) -> Self
|
||||
{
|
||||
self.spacing_y = s;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the padding on all sides (default 0.0).
|
||||
pub fn padding( mut self, p: f32 ) -> Self
|
||||
{
|
||||
self.padding = p;
|
||||
self
|
||||
}
|
||||
|
||||
/// Compute the preferred size given an available width.
|
||||
pub fn preferred_size( &self, max_width: f32, canvas: &Canvas ) -> (f32, f32)
|
||||
{
|
||||
if self.children.is_empty() || self.columns == 0
|
||||
{
|
||||
return ( max_width, 0.0 );
|
||||
}
|
||||
let cols = self.columns;
|
||||
let inner_w = (max_width - self.padding * 2.0).max( 0.0 );
|
||||
let cell_w = (inner_w - self.spacing_x * (cols as f32 - 1.0)).max( 0.0 ) / cols as f32;
|
||||
let row_count = (self.children.len() + cols - 1) / cols;
|
||||
|
||||
let mut total_h = self.padding * 2.0;
|
||||
for row in 0..row_count
|
||||
{
|
||||
let start = row * cols;
|
||||
let end = (start + cols).min( self.children.len() );
|
||||
let row_h = self.children[start..end]
|
||||
.iter()
|
||||
.map( |c| c.preferred_size( cell_w, canvas ).1 )
|
||||
.fold( 0.0_f32, f32::max );
|
||||
total_h += row_h;
|
||||
if row + 1 < row_count { total_h += self.spacing_y; }
|
||||
}
|
||||
( max_width, total_h )
|
||||
}
|
||||
|
||||
/// Compute child rects. Returns `(child_rect, index_in_children)` pairs.
|
||||
pub fn layout( &self, rect: Rect, canvas: &Canvas ) -> Vec<(Rect, usize)>
|
||||
{
|
||||
if self.children.is_empty() || self.columns == 0
|
||||
{
|
||||
return Vec::new();
|
||||
}
|
||||
let cols = self.columns;
|
||||
let inner_w = (rect.width - self.padding * 2.0).max( 0.0 );
|
||||
let cell_w = (inner_w - self.spacing_x * (cols as f32 - 1.0)).max( 0.0 ) / cols as f32;
|
||||
let x0 = rect.x + self.padding;
|
||||
let mut y = rect.y + self.padding;
|
||||
|
||||
let row_count = (self.children.len() + cols - 1) / cols;
|
||||
let mut out = Vec::with_capacity( self.children.len() );
|
||||
|
||||
for row in 0..row_count
|
||||
{
|
||||
let start = row * cols;
|
||||
let end = (start + cols).min( self.children.len() );
|
||||
let row_h = self.children[start..end]
|
||||
.iter()
|
||||
.map( |c| c.preferred_size( cell_w, canvas ).1 )
|
||||
.fold( 0.0_f32, f32::max );
|
||||
|
||||
for col in 0..(end - start)
|
||||
{
|
||||
let x = x0 + col as f32 * (cell_w + self.spacing_x);
|
||||
let crect = Rect { x, y, width: cell_w, height: row_h };
|
||||
out.push( ( crect, start + col ) );
|
||||
}
|
||||
y += row_h + self.spacing_y;
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
pub( crate ) fn map_msg<U>( self, f: &crate::widget::MapFn<Msg, U> ) -> WrapGrid<U>
|
||||
where
|
||||
U: Clone + 'static,
|
||||
Msg: 'static,
|
||||
{
|
||||
WrapGrid
|
||||
{
|
||||
children: self.children.into_iter().map( |c| c.map_arc( f ) ).collect(),
|
||||
columns: self.columns,
|
||||
spacing_x: self.spacing_x,
|
||||
spacing_y: self.spacing_y,
|
||||
padding: self.padding,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Msg: Clone + 'static> From<WrapGrid<Msg>> for Element<Msg>
|
||||
{
|
||||
fn from( g: WrapGrid<Msg> ) -> Self
|
||||
{
|
||||
Element::WrapGrid( g )
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests
|
||||
{
|
||||
use super::*;
|
||||
use crate::render::Canvas;
|
||||
use crate::layout::spacer::spacer;
|
||||
|
||||
|
||||
fn canvas() -> Canvas { Canvas::new( 1, 1 ) }
|
||||
|
||||
// Helper: build a grid of N spacer children with the given settings.
|
||||
fn spacer_grid( cols: usize, n: usize, spacing: f32, padding: f32 ) -> WrapGrid<()>
|
||||
{
|
||||
let mut g = grid( cols ).spacing( spacing ).padding( padding );
|
||||
for _ in 0..n { g = g.push( spacer() ); }
|
||||
g
|
||||
}
|
||||
|
||||
// --- preferred_size ---
|
||||
|
||||
#[test]
|
||||
fn empty_grid_height_is_zero()
|
||||
{
|
||||
let g: WrapGrid<()> = grid( 4 );
|
||||
let ( _, h ) = g.preferred_size( 400.0, &canvas() );
|
||||
assert_eq!( h, 0.0 );
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preferred_width_equals_max_width()
|
||||
{
|
||||
let g = spacer_grid( 4, 8, 0.0, 0.0 );
|
||||
let ( w, _ ) = g.preferred_size( 320.0, &canvas() );
|
||||
assert_eq!( w, 320.0 );
|
||||
}
|
||||
|
||||
// --- layout: cell widths ---
|
||||
|
||||
#[test]
|
||||
fn cell_width_no_spacing_no_padding()
|
||||
{
|
||||
// 400px / 4 cols = 100px each
|
||||
let g = spacer_grid( 4, 4, 0.0, 0.0 );
|
||||
let c = canvas();
|
||||
let rect = Rect { x: 0.0, y: 0.0, width: 400.0, height: 200.0 };
|
||||
let rects = g.layout( rect, &c );
|
||||
assert_eq!( rects.len(), 4 );
|
||||
for ( r, _ ) in &rects { assert!( (r.width - 100.0).abs() < 0.01 ); }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cell_width_with_spacing()
|
||||
{
|
||||
// (400 - 3 * 10) / 4 = 370 / 4 = 92.5
|
||||
let g = spacer_grid( 4, 4, 10.0, 0.0 );
|
||||
let c = canvas();
|
||||
let rect = Rect { x: 0.0, y: 0.0, width: 400.0, height: 200.0 };
|
||||
let rects = g.layout( rect, &c );
|
||||
for ( r, _ ) in &rects { assert!( (r.width - 92.5).abs() < 0.01 ); }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cell_width_with_padding()
|
||||
{
|
||||
// inner = 400 - 2*20 = 360; 360 / 4 = 90
|
||||
let g = spacer_grid( 4, 4, 0.0, 20.0 );
|
||||
let c = canvas();
|
||||
let rect = Rect { x: 0.0, y: 0.0, width: 400.0, height: 200.0 };
|
||||
let rects = g.layout( rect, &c );
|
||||
for ( r, _ ) in &rects { assert!( (r.width - 90.0).abs() < 0.01 ); }
|
||||
}
|
||||
|
||||
// --- layout: child count and indices ---
|
||||
|
||||
#[test]
|
||||
fn layout_yields_one_rect_per_child()
|
||||
{
|
||||
let g = spacer_grid( 4, 7, 0.0, 0.0 );
|
||||
let c = canvas();
|
||||
let rect = Rect { x: 0.0, y: 0.0, width: 400.0, height: 400.0 };
|
||||
let rects = g.layout( rect, &c );
|
||||
assert_eq!( rects.len(), 7 );
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn layout_indices_are_sequential()
|
||||
{
|
||||
let g = spacer_grid( 3, 5, 0.0, 0.0 );
|
||||
let c = canvas();
|
||||
let rect = Rect { x: 0.0, y: 0.0, width: 300.0, height: 300.0 };
|
||||
let rects = g.layout( rect, &c );
|
||||
let indices: Vec<usize> = rects.iter().map( |( _, i )| *i ).collect();
|
||||
assert_eq!( indices, vec![ 0, 1, 2, 3, 4 ] );
|
||||
}
|
||||
|
||||
// --- layout: column x-positions ---
|
||||
|
||||
#[test]
|
||||
fn column_x_positions_no_spacing()
|
||||
{
|
||||
// 300px / 3 cols = 100px each, starting at x=0
|
||||
let g = spacer_grid( 3, 3, 0.0, 0.0 );
|
||||
let c = canvas();
|
||||
let rect = Rect { x: 0.0, y: 0.0, width: 300.0, height: 100.0 };
|
||||
let rects = g.layout( rect, &c );
|
||||
let xs: Vec<f32> = rects.iter().map( |( r, _ )| r.x ).collect();
|
||||
assert!( (xs[0] - 0.0).abs() < 0.01 );
|
||||
assert!( (xs[1] - 100.0).abs() < 0.01 );
|
||||
assert!( (xs[2] - 200.0).abs() < 0.01 );
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn column_x_positions_with_spacing()
|
||||
{
|
||||
// (300 - 2*10) / 3 = 280/3 ≈ 93.33; x[0]=0, x[1]=103.33, x[2]=206.67
|
||||
let g = spacer_grid( 3, 3, 10.0, 0.0 );
|
||||
let c = canvas();
|
||||
let rect = Rect { x: 0.0, y: 0.0, width: 300.0, height: 100.0 };
|
||||
let rects = g.layout( rect, &c );
|
||||
let cell_w = 280.0_f32 / 3.0;
|
||||
let xs: Vec<f32> = rects.iter().map( |( r, _ )| r.x ).collect();
|
||||
assert!( (xs[0] - 0.0).abs() < 0.01 );
|
||||
assert!( (xs[1] - (cell_w + 10.0)).abs() < 0.01 );
|
||||
assert!( (xs[2] - (2.0 * (cell_w + 10.0))).abs() < 0.01 );
|
||||
}
|
||||
|
||||
// --- layout: partial last row ---
|
||||
|
||||
#[test]
|
||||
fn partial_last_row_has_correct_count()
|
||||
{
|
||||
// 7 children, 4 cols => row 0: 4, row 1: 3.
|
||||
let g = spacer_grid( 4, 7, 0.0, 0.0 );
|
||||
let c = canvas();
|
||||
let rect = Rect { x: 0.0, y: 0.0, width: 400.0, height: 400.0 };
|
||||
let rects = g.layout( rect, &c );
|
||||
assert_eq!( rects.len(), 7 );
|
||||
for ( r, _ ) in &rects[..4] { assert!( r.y.abs() < 0.01 ); }
|
||||
}
|
||||
|
||||
// --- layout: rect origin offset ---
|
||||
|
||||
#[test]
|
||||
fn layout_respects_rect_origin()
|
||||
{
|
||||
let g = spacer_grid( 2, 2, 0.0, 0.0 );
|
||||
let c = canvas();
|
||||
let rect = Rect { x: 50.0, y: 30.0, width: 200.0, height: 100.0 };
|
||||
let rects = g.layout( rect, &c );
|
||||
assert!( (rects[0].0.x - 50.0).abs() < 0.01 );
|
||||
assert!( (rects[0].0.y - 30.0).abs() < 0.01 );
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a grid layout with the given number of columns.
|
||||
///
|
||||
/// Use [`.push()`](WrapGrid::push), [`.spacing()`](WrapGrid::spacing), and
|
||||
/// [`.padding()`](WrapGrid::padding) to populate and style the grid.
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use ltk::{ button, grid, WrapGrid };
|
||||
/// # #[ derive( Clone ) ] enum Msg { A }
|
||||
/// # fn _ex() -> WrapGrid<Msg> {
|
||||
/// grid( 4 ).padding( 16.0 ).spacing( 8.0 ).push( button( "A" ).on_press( Msg::A ) )
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn grid<Msg: Clone>( columns: usize ) -> WrapGrid<Msg>
|
||||
{
|
||||
WrapGrid
|
||||
{
|
||||
children: Vec::new(),
|
||||
columns,
|
||||
spacing_x: 8.0,
|
||||
spacing_y: 8.0,
|
||||
padding: 0.0,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user