layout: adaptive grid and vertical flex; enforce the mechanical style rules
Some checks failed
CI / build + test (push) Has been cancelled
CI / cargo audit (push) Has been cancelled

grid_min_cell( width ) adds the adaptive mode WrapGrid was missing: instead of a fixed column count, the count is derived at layout time from the available width so every cell is at least `width` wide (any Length, so a fluid threshold works; never fewer than one column), re-derived on every resize. Cells then share the width equally, and WrapGrid::max_columns( n ) caps the derived count so cells grow instead of multiplying on very wide surfaces — the cap only applies to the adaptive mode, grid( n ) keeps the fixed behaviour untouched. Four new layout tests cover width derivation, spacing accounting, the one-column floor and the cap; the row-wrap assertions check X positions rather than Y because zero-height spacer children produce zero-height rows.
flex( child ) now distributes leftover height inside a Column, mirroring its leftover-width behaviour in a Row: flex children join the weight pool alongside weight-only spacers and y-scrolls, draw their child inside the allocated share at full inner width, and contribute zero to the column's natural height exactly like a row counts flex width. This closes the documented "vertical flex is not yet implemented" gap; the Flex rustdoc and the widgets.md entry now describe both axes.
Add scripts/style-check.sh and a make stylecheck target, wired into CI: mechanical verification of the grep-checkable subset of code_style_guide.md — tab indentation and spaces inside attribute brackets — with comment lines skipped so prose may cite raw attribute syntax. To turn the check on green, the 82 unspaced attribute sites across 24 files (#[test], #[derive(...)], #[cfg(...)], #[allow(...)]) were normalized to the spaced form. CONTRIBUTING and the style guide reference the new target; brace placement and paren spacing remain review concerns.
This commit is contained in:
2026-07-30 22:20:21 +02:00
parent 1fd697aa6d
commit a7f953ca42
33 changed files with 380 additions and 115 deletions

View File

@@ -26,7 +26,7 @@ use crate::render::Canvas;
use crate::types::{ Rect, WidgetId };
use crate::widget::Element;
#[cfg(test)]
#[ cfg( test ) ]
mod tests;
pub struct Carousel<Msg: Clone>

View File

@@ -17,7 +17,7 @@ fn three_spacer_carousel() -> Carousel<()>
.push( spacer() )
}
#[test]
#[ test ]
fn empty_layout_is_empty()
{
let c: Carousel<()> = carousel();
@@ -25,7 +25,7 @@ fn empty_layout_is_empty()
assert!( c.layout( rect, &canvas() ).is_empty() );
}
#[test]
#[ test ]
fn first_child_centred_at_offset_zero()
{
let c = three_spacer_carousel();
@@ -36,7 +36,7 @@ fn first_child_centred_at_offset_zero()
assert!( ( rects[0].0.width - 80.0 ).abs() < 0.01 );
}
#[test]
#[ test ]
fn children_strided_by_child_width_plus_gap()
{
let c = carousel::<()>()
@@ -50,7 +50,7 @@ fn children_strided_by_child_width_plus_gap()
assert!( ( rects[1].0.x - ( rects[0].0.x + 108.0 ) ).abs() < 0.01 );
}
#[test]
#[ test ]
fn snap_offset_centres_target_index()
{
let c = three_spacer_carousel();
@@ -58,7 +58,7 @@ fn snap_offset_centres_target_index()
assert!( ( c.snap_offset( 100.0, 2 ) - ( -160.0 ) ).abs() < 0.01 );
}
#[test]
#[ test ]
fn focused_index_rounds_to_nearest_tile()
{
let mut c = three_spacer_carousel();
@@ -68,7 +68,7 @@ fn focused_index_rounds_to_nearest_tile()
assert_eq!( c.focused_index( 100.0 ), 1 );
}
#[test]
#[ test ]
fn focused_index_clamps_to_valid_range()
{
let mut c = three_spacer_carousel();
@@ -78,7 +78,7 @@ fn focused_index_clamps_to_valid_range()
assert_eq!( c.focused_index( 100.0 ), 2 );
}
#[test]
#[ test ]
fn offset_shifts_all_children_horizontally()
{
let c = three_spacer_carousel().offset( -25.0 );
@@ -90,7 +90,7 @@ fn offset_shifts_all_children_horizontally()
assert!( ( rects[1].0.x - rects[0].0.x - 80.0 ).abs() < 0.01 );
}
#[test]
#[ test ]
fn focused_width_frac_is_clamped_to_unit_range()
{
let c: Carousel<()> = carousel().focused_width_frac( 5.0 ).push( spacer() );
@@ -99,7 +99,7 @@ fn focused_width_frac_is_clamped_to_unit_range()
assert!( c2.focused_width_frac > 0.0 );
}
#[test]
#[ test ]
fn children_use_full_rect_height()
{
let c = three_spacer_carousel();

View File

@@ -7,7 +7,7 @@ use super::Element;
mod theme;
#[cfg(test)]
#[ cfg( test ) ]
mod tests;
/// A two-state opt-in control with a square box and a check glyph.

View File

@@ -3,7 +3,7 @@
use super::*;
#[test]
#[ test ]
fn checkbox_default_state()
{
let c = checkbox::<()>( true );
@@ -11,7 +11,7 @@ fn checkbox_default_state()
assert!( c.on_toggle.is_none() );
}
#[test]
#[ test ]
fn checkbox_unchecked()
{
let c = checkbox::<()>( false );

View File

@@ -4,16 +4,19 @@
use crate::render::Canvas;
use super::Element;
/// Wraps an [`Element`] so that a [`Row`](crate::layout::row::Row) treats it
/// like a [`Spacer`](crate::layout::spacer::Spacer) for leftover-width
/// distribution, but draws the child inside the allocated rect.
/// Wraps an [`Element`] so its parent treats it like a
/// [`Spacer`](crate::layout::spacer::Spacer) for leftover-space
/// distribution, but draws the child inside the allocated rect: leftover
/// **width** inside a [`Row`](crate::layout::row::Row), leftover
/// **height** inside a [`Column`](crate::layout::column::Column).
///
/// Use this to make a non-trivial child fill remaining width without resorting
/// to a hard-coded `max_width`. The row computes how much width is left after
/// the fixed-size siblings, splits it across all flex / spacer children
/// proportionally to their `weight`, and gives the flex its share. The wrapped
/// child sees that share as its layout rect — text inside it triggers its own
/// elide path on overflow, columns adjust their inner width, etc.
/// Use this to make a non-trivial child fill remaining space without
/// resorting to a hard-coded size. The parent computes how much of its
/// main axis is left after the fixed-size siblings, splits it across all
/// flex / spacer children proportionally to their `weight`, and gives
/// the flex its share. The wrapped child sees that share as its layout
/// rect — text inside it triggers its own elide path on overflow,
/// columns adjust their inner width, etc.
///
/// ```rust,no_run
/// # use ltk::{ column, flex, row, Element };
@@ -30,10 +33,6 @@ use super::Element;
/// # }
/// ```
///
/// Currently only [`Row`](crate::layout::row::Row) honours flex distribution.
/// Inside a [`Column`](crate::layout::column::Column) a flex child behaves
/// like a regular zero-width child along the main axis — vertical flex is not
/// yet implemented.
pub struct Flex<Msg: Clone>
{
pub( crate ) child: Box<Element<Msg>>,
@@ -51,9 +50,9 @@ impl<Msg: Clone> Flex<Msg>
}
}
/// Relative weight when sharing leftover width with other flex / spacer
/// children in the same row (default `1`). A flex with `weight = 2`
/// claims twice the space of a sibling with `weight = 1`.
/// Relative weight when sharing leftover space with other flex / spacer
/// children in the same row or column (default `1`). A flex with
/// `weight = 2` claims twice the space of a sibling with `weight = 1`.
pub fn weight( mut self, w: u32 ) -> Self
{
self.weight = w;

View File

@@ -9,7 +9,7 @@ use super::Element;
mod theme;
#[cfg(test)]
#[ cfg( test ) ]
mod tests;
/// A row inside a list with a primary label and optional subtitle / trailing

View File

@@ -3,7 +3,7 @@
use super::*;
#[test]
#[ test ]
fn list_item_default()
{
let l = list_item::<()>( "Test" );

View File

@@ -7,7 +7,7 @@ use super::Element;
mod theme;
#[cfg(test)]
#[ cfg( test ) ]
mod tests;
/// A linear progress indicator for determinate operations.

View File

@@ -3,7 +3,7 @@
use super::*;
#[test]
#[ test ]
fn value_clamped_on_creation()
{
let p = progress_bar( 1.5 );
@@ -12,7 +12,7 @@ fn value_clamped_on_creation()
assert_eq!( p.value, 0.0 );
}
#[test]
#[ test ]
fn preferred_width_fills_available()
{
let p = progress_bar( 0.5 );

View File

@@ -7,7 +7,7 @@ use super::Element;
mod theme;
#[cfg(test)]
#[ cfg( test ) ]
mod tests;
/// One option inside a mutually-exclusive group.

View File

@@ -3,7 +3,7 @@
use super::*;
#[test]
#[ test ]
fn radio_default_state()
{
let r = radio::<()>( true );
@@ -11,7 +11,7 @@ fn radio_default_state()
assert!( r.on_select.is_none() );
}
#[test]
#[ test ]
fn radio_unselected()
{
let r = radio::<()>( false );

View File

@@ -5,7 +5,7 @@ use crate::render::Canvas;
use crate::types::WidgetId;
use crate::widget::Element;
#[cfg(test)]
#[ cfg( test ) ]
mod tests;
/// Which axes a `Scroll` viewport allows to move along. Determines

View File

@@ -3,46 +3,46 @@
use super::clamp_offset;
#[test]
#[ 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]
#[ test ]
fn offset_clamped_to_zero_when_negative()
{
assert_eq!( clamp_offset( -10.0, 600.0, 400.0 ), 0.0 );
}
#[test]
#[ 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]
#[ 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]
#[ test ]
fn zero_offset_stays_zero()
{
assert_eq!( clamp_offset( 0.0, 600.0, 400.0 ), 0.0 );
}
#[test]
#[ test ]
fn exact_max_offset_is_valid()
{
assert_eq!( clamp_offset( 200.0, 600.0, 400.0 ), 200.0 );
}
#[test]
#[ test ]
fn content_equal_to_viewport_gives_zero_max()
{
// No overflow — max = 0

View File

@@ -142,5 +142,5 @@ impl<Msg: Clone> From<Separator> for Element<Msg>
}
}
#[cfg(test)]
#[ cfg( test ) ]
mod tests;

View File

@@ -3,7 +3,7 @@
use super::*;
#[test]
#[ test ]
fn default_thickness_and_pad_follow_the_mode()
{
let s = separator();
@@ -11,7 +11,7 @@ fn default_thickness_and_pad_follow_the_mode()
assert_eq!( s.pad_v, None );
}
#[test]
#[ test ]
fn explicit_zero_pad_v_is_flush_not_the_mode_default()
{
// The Option repr lets `0.0` mean a real flush divider, distinct from
@@ -22,7 +22,7 @@ fn explicit_zero_pad_v_is_flush_not_the_mode_default()
assert_eq!( s.preferred_size( 200.0, &canvas ).1, canvas.geom_px( theme::THICKNESS ) );
}
#[test]
#[ test ]
fn preferred_height_includes_padding()
{
let _g = crate::TEST_GLOBALS_LOCK.lock().unwrap_or_else( |e| e.into_inner() );

View File

@@ -4,7 +4,7 @@
use super::*;
use crate::types::Rect;
#[test]
#[ test ]
fn value_clamped_on_creation()
{
let s = slider::<()>( 1.5 );
@@ -13,7 +13,7 @@ fn value_clamped_on_creation()
assert_eq!( s.value, 0.0 );
}
#[test]
#[ test ]
fn value_from_x_left_edge()
{
let s = slider::<()>( 0.5 );
@@ -22,7 +22,7 @@ fn value_from_x_left_edge()
assert_eq!( v, 0.0 );
}
#[test]
#[ test ]
fn value_from_x_right_edge()
{
let s = slider::<()>( 0.5 );
@@ -31,7 +31,7 @@ fn value_from_x_right_edge()
assert_eq!( v, 1.0 );
}
#[test]
#[ test ]
fn value_from_x_center()
{
let s = slider::<()>( 0.0 );
@@ -40,7 +40,7 @@ fn value_from_x_center()
assert!( (v - 0.5).abs() < 0.1 );
}
#[test]
#[ test ]
fn axis_dispatch_horizontal_uses_x()
{
let rect = Rect { x: 0.0, y: 0.0, width: 200.0, height: 36.0 };
@@ -53,7 +53,7 @@ fn axis_dispatch_horizontal_uses_x()
assert_eq!( v, 1.0 );
}
#[test]
#[ test ]
fn axis_dispatch_vertical_uses_y()
{
let rect = Rect { x: 0.0, y: 0.0, width: 56.0, height: 160.0 };
@@ -67,7 +67,7 @@ fn axis_dispatch_vertical_uses_y()
assert_eq!( v, 1.0 );
}
#[test]
#[ test ]
fn value_from_x_respects_thumb_pad()
{
let rect = Rect { x: 0.0, y: 0.0, width: 100.0, height: 36.0 };
@@ -82,14 +82,14 @@ fn value_from_x_respects_thumb_pad()
);
}
#[test]
#[ test ]
fn track_paint_default_is_none()
{
let s = slider::<()>( 0.5 );
assert!( s.track_paint.is_none() );
}
#[test]
#[ test ]
fn track_paint_builder_stores_paint()
{
use crate::theme::{ ColorStop, GradientSpace, LinearGradient, Paint };

View File

@@ -7,7 +7,7 @@ use super::Element;
mod theme;
#[cfg(test)]
#[ cfg( test ) ]
mod tests;
/// A two-state on / off switch.

View File

@@ -3,7 +3,7 @@
use super::*;
#[test]
#[ test ]
fn toggle_default_state()
{
let t = toggle::<()>( true );
@@ -12,7 +12,7 @@ fn toggle_default_state()
assert!( t.label.is_none() );
}
#[test]
#[ test ]
fn toggle_off_state()
{
let t = toggle::<()>( false );