First commit. Version 0.1.0
This commit is contained in:
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() );
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user