add Carousel widget and WrapGrid::centre_last_row
Forge's app switcher needs two layouts the existing widget set didn't cover. The desktop grid wants a partial last row centred under the rows above (3 tiles → row 1: two, row 2: one centred) so a 7-of-9 leftover band reads balanced rather than left-aligned. The mobile variant wants a horizontal carousel where the focused tile sits centred in the viewport at a configurable fraction of its width and its neighbours peek out on the sides at a fixed gap.
Extend `WrapGrid` with `centre_last_row( bool )`. When set, layout offsets a row that has fewer than `columns` children by `(missing * (cell_w + spacing)) / 2` so it stays centred inside the content rect. Defaults to false; every existing call site continues to land tiles flush-left. Covered by three layout tests (centred partial row, full row no-op, off-by-default).
Add the `Carousel` widget at `src/widget/carousel/`. It is a pure layout primitive: `focused_width_frac` (0.05–1.0, clamped), `gap` and `offset` are owned by the caller, leaving drag / inertia / snap policy to the host so the compositor can plug in its existing touch pipeline. Each child gets a rect at `base_x + idx * (child_w + gap)` and the full viewport height; `snap_offset( viewport_w, idx )` translates index to centring offset and `focused_index( viewport_w )` rounds the current offset back to the nearest tile. Plumbed into `Element::Carousel` with the matching arms in `widget/element.rs` and walker in `draw/layout.rs`; re-exported as `ltk::{ Carousel, carousel }`. Covered by nine unit tests (layout, offset shift, snap / focus round-trip, frac clamp, child height) plus a `cargo run --example carousel` demo with Prev / Next / arrow-key navigation against an external offset state. The example is wired into the `examples` Makefile target.
Updates the widget catalogue and the `widget/mod.rs` landing comment to list the carousel under "Clipping wrappers" and to mention `centre_last_row` in the grid section.
This commit is contained in:
@@ -32,15 +32,18 @@ use crate::widget::Element;
|
||||
pub struct WrapGrid<Msg: Clone>
|
||||
{
|
||||
/// Child widgets laid out in row-major order.
|
||||
pub children: Vec<Element<Msg>>,
|
||||
pub children: Vec<Element<Msg>>,
|
||||
/// Number of columns per row.
|
||||
pub columns: usize,
|
||||
pub columns: usize,
|
||||
/// Horizontal gap between cells (pixels).
|
||||
pub spacing_x: f32,
|
||||
pub spacing_x: f32,
|
||||
/// Vertical gap between rows (pixels).
|
||||
pub spacing_y: f32,
|
||||
pub spacing_y: f32,
|
||||
/// Padding on all sides (pixels).
|
||||
pub padding: f32,
|
||||
pub padding: f32,
|
||||
/// When `true`, a partial last row is centred horizontally within
|
||||
/// the grid's content rect instead of being left-aligned.
|
||||
pub centre_last_row: bool,
|
||||
}
|
||||
|
||||
impl<Msg: Clone> WrapGrid<Msg>
|
||||
@@ -81,6 +84,14 @@ impl<Msg: Clone> WrapGrid<Msg>
|
||||
self
|
||||
}
|
||||
|
||||
/// Centre a partial last row horizontally inside the content rect.
|
||||
/// Default is `false` (left-aligned, like other grids).
|
||||
pub fn centre_last_row( mut self, yes: bool ) -> Self
|
||||
{
|
||||
self.centre_last_row = yes;
|
||||
self
|
||||
}
|
||||
|
||||
/// Compute the preferred size given an available width.
|
||||
pub fn preferred_size( &self, max_width: f32, canvas: &Canvas ) -> (f32, f32)
|
||||
{
|
||||
@@ -133,9 +144,16 @@ impl<Msg: Clone> WrapGrid<Msg>
|
||||
.map( |c| c.preferred_size( cell_w, canvas ).1 )
|
||||
.fold( 0.0_f32, f32::max );
|
||||
|
||||
for col in 0..(end - start)
|
||||
let items_in_row = end - start;
|
||||
let row_offset = if self.centre_last_row && items_in_row < cols
|
||||
{
|
||||
let x = x0 + col as f32 * (cell_w + self.spacing_x);
|
||||
let missing = (cols - items_in_row) as f32;
|
||||
missing * (cell_w + self.spacing_x) / 2.0
|
||||
} else { 0.0 };
|
||||
|
||||
for col in 0..items_in_row
|
||||
{
|
||||
let x = x0 + row_offset + col as f32 * (cell_w + self.spacing_x);
|
||||
let crect = Rect { x, y, width: cell_w, height: row_h };
|
||||
out.push( ( crect, start + col ) );
|
||||
}
|
||||
@@ -151,11 +169,12 @@ impl<Msg: Clone> WrapGrid<Msg>
|
||||
{
|
||||
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,
|
||||
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,
|
||||
centre_last_row: self.centre_last_row,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -320,6 +339,43 @@ mod tests
|
||||
assert!( (rects[0].0.x - 50.0).abs() < 0.01 );
|
||||
assert!( (rects[0].0.y - 30.0).abs() < 0.01 );
|
||||
}
|
||||
|
||||
// --- layout: centre_last_row ---
|
||||
|
||||
#[test]
|
||||
fn last_row_centred_when_partial()
|
||||
{
|
||||
// 3 children, 2 cols => row 0: 2 items, row 1: 1 item centred.
|
||||
// cell_w = 200/2 = 100; centred-offset = (2-1)*100/2 = 50.
|
||||
let g = spacer_grid( 2, 3, 0.0, 0.0 ).centre_last_row( true );
|
||||
let c = canvas();
|
||||
let rect = Rect { x: 0.0, y: 0.0, width: 200.0, height: 400.0 };
|
||||
let rects = g.layout( rect, &c );
|
||||
assert!( (rects[2].0.x - 50.0).abs() < 0.01 );
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn centre_last_row_noop_on_full_row()
|
||||
{
|
||||
// 4 children, 2 cols => both rows full; nothing to centre.
|
||||
let g = spacer_grid( 2, 4, 0.0, 0.0 ).centre_last_row( true );
|
||||
let c = canvas();
|
||||
let rect = Rect { x: 0.0, y: 0.0, width: 200.0, height: 400.0 };
|
||||
let rects = g.layout( rect, &c );
|
||||
assert!( rects[2].0.x.abs() < 0.01 );
|
||||
assert!( (rects[3].0.x - 100.0).abs() < 0.01 );
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn centre_last_row_off_by_default()
|
||||
{
|
||||
// Same case as above but without the flag — last item stays at x=0.
|
||||
let g = spacer_grid( 2, 3, 0.0, 0.0 );
|
||||
let c = canvas();
|
||||
let rect = Rect { x: 0.0, y: 0.0, width: 200.0, height: 400.0 };
|
||||
let rects = g.layout( rect, &c );
|
||||
assert!( rects[2].0.x.abs() < 0.01 );
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a grid layout with the given number of columns.
|
||||
@@ -338,10 +394,11 @@ pub fn grid<Msg: Clone>( columns: usize ) -> WrapGrid<Msg>
|
||||
{
|
||||
WrapGrid
|
||||
{
|
||||
children: Vec::new(),
|
||||
children: Vec::new(),
|
||||
columns,
|
||||
spacing_x: 8.0,
|
||||
spacing_y: 8.0,
|
||||
padding: 0.0,
|
||||
spacing_x: 8.0,
|
||||
spacing_y: 8.0,
|
||||
padding: 0.0,
|
||||
centre_last_row: false,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user