layout: adaptive grid and vertical flex; enforce the mechanical style rules
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:
@@ -71,6 +71,9 @@ jobs:
|
|||||||
- name: Markdown doctests
|
- name: Markdown doctests
|
||||||
run: ./scripts/doctest-md.sh
|
run: ./scripts/doctest-md.sh
|
||||||
|
|
||||||
|
- name: Style check
|
||||||
|
run: ./scripts/style-check.sh
|
||||||
|
|
||||||
audit:
|
audit:
|
||||||
name: cargo audit
|
name: cargo audit
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ All notable changes to `ltk` are documented here. The format is based on [Keep a
|
|||||||
- **Responsive sizing system** with two selectable modes via `WidgetScaling` (`Fluid` / `Physical`; `set_widget_scaling` / `widget_scaling`, default `Fluid`). New `Length` constructors — `orient( portrait, landscape )` (a percentage of the width in portrait, of the height in landscape), `fluid( px )` (surface-proportional, calibrated against `set_fluid_reference` and bounded by `FLUID_MIN` / `FLUID_MAX`), `dp( px )` (constant physical size scaled by `set_density` / `density`), and `widget( px )` (picks fluid or dp per the active mode). `Canvas::geom_px` (geometry, physical layout space) and `Canvas::font_px` (font, bridging the logical / physical split per mode) give widgets and apps one resolution path.
|
- **Responsive sizing system** with two selectable modes via `WidgetScaling` (`Fluid` / `Physical`; `set_widget_scaling` / `widget_scaling`, default `Fluid`). New `Length` constructors — `orient( portrait, landscape )` (a percentage of the width in portrait, of the height in landscape), `fluid( px )` (surface-proportional, calibrated against `set_fluid_reference` and bounded by `FLUID_MIN` / `FLUID_MAX`), `dp( px )` (constant physical size scaled by `set_density` / `density`), and `widget( px )` (picks fluid or dp per the active mode). `Canvas::geom_px` (geometry, physical layout space) and `Canvas::font_px` (font, bridging the logical / physical split per mode) give widgets and apps one resolution path.
|
||||||
- **`Button::font_size` / `height` / `width`** and **`TextEdit::height`** builders, all `impl Into<Length>`, so control boxes scale with the surface. `Text::line_height( mult )` opens the gap between wrapped lines. `Separator::pad_v` (with `Length::px( 0.0 )` for a flush divider).
|
- **`Button::font_size` / `height` / `width`** and **`TextEdit::height`** builders, all `impl Into<Length>`, so control boxes scale with the surface. `Text::line_height( mult )` opens the gap between wrapped lines. `Separator::pad_v` (with `Length::px( 0.0 )` for a flush divider).
|
||||||
- **Performance guardrails**: opt-in diagnostics via `LTK_PERF_WARN=1` (stuck animation, sustained software-render animation, low `poll_interval`) and a ~30 Hz software-animation cap overridable with `App::cap_software_animation`.
|
- **Performance guardrails**: opt-in diagnostics via `LTK_PERF_WARN=1` (stuck animation, sustained software-render animation, low `poll_interval`) and a ~30 Hz software-animation cap overridable with `App::cap_software_animation`.
|
||||||
|
- **`grid_min_cell( width )` and `WrapGrid::max_columns( n )`** — adaptive grid: the column count is derived at layout time from the available width so every cell is at least `width` wide (any `Length`; never fewer than one column), re-derived on every resize; `max_columns` caps the count so cells grow instead of multiplying on wide surfaces. `grid( n )` keeps the fixed-count behaviour.
|
||||||
|
- **Vertical flex: `flex( child )` now distributes leftover height inside a `Column`**, mirroring its leftover-width behaviour in a `Row` — weights split the spare space between flex / spacer siblings, the child draws inside the allocated share, and it contributes zero to the column's natural height like a weight-only spacer.
|
||||||
|
- **`make stylecheck` / `scripts/style-check.sh`** — mechanical checks for the grep-verifiable subset of the style guide (tab indentation, spaces inside attribute brackets), wired into CI. The whole tree was normalized to pass (82 attribute sites).
|
||||||
- **`ltk::orientation()` / `viewport_size()` / `set_viewport_size`** and the `Orientation` enum — the runtime records the main surface's physical dimensions on every configure, so `view()` can branch a layout on portrait vs landscape (`match ltk::orientation() { … }`) without tracking `on_resize` by hand; the portrait/landscape rule matches `Length::orient` (square counts as portrait). Embedders driving `core::UiSurface` directly call `set_viewport_size` themselves. `examples/clip_path.rs` demonstrates it.
|
- **`ltk::orientation()` / `viewport_size()` / `set_viewport_size`** and the `Orientation` enum — the runtime records the main surface's physical dimensions on every configure, so `view()` can branch a layout on portrait vs landscape (`match ltk::orientation() { … }`) without tracking `on_resize` by hand; the portrait/landscape rule matches `Length::orient` (square counts as portrait). Embedders driving `core::UiSurface` directly call `set_viewport_size` themselves. `examples/clip_path.rs` demonstrates it.
|
||||||
- **`test-support` Cargo feature** gates the `test_support` module so third-party builds never see it (ltk's own `make test` enables it).
|
- **`test-support` Cargo feature** gates the `test_support` module so third-party builds never see it (ltk's own `make test` enables it).
|
||||||
|
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ The `Makefile` wraps the common targets:
|
|||||||
make all # cargo build --release
|
make all # cargo build --release
|
||||||
make test # cargo test --features test-support
|
make test # cargo test --features test-support
|
||||||
make doctest-md # typecheck the Rust snippets in docs/*.md
|
make doctest-md # typecheck the Rust snippets in docs/*.md
|
||||||
|
make stylecheck # mechanical style checks (tabs, attribute spacing)
|
||||||
make audit # cargo audit (installs cargo-audit on first run)
|
make audit # cargo audit (installs cargo-audit on first run)
|
||||||
make doc # cargo doc --no-deps
|
make doc # cargo doc --no-deps
|
||||||
make examples # run every example under examples/ in turn
|
make examples # run every example under examples/ in turn
|
||||||
|
|||||||
7
Makefile
7
Makefile
@@ -5,7 +5,7 @@ DOCDIR ?= /usr/share/doc/libltk-doc/html
|
|||||||
# ignore filesystem entries with the same name — without this `examples`
|
# ignore filesystem entries with the same name — without this `examples`
|
||||||
# silently no-ops because the `examples/` directory exists, and `doc`
|
# silently no-ops because the `examples/` directory exists, and `doc`
|
||||||
# would do the same once `target/doc` is around.
|
# would do the same once `target/doc` is around.
|
||||||
.PHONY: all test doctest-md audit doc install examples clean distclean
|
.PHONY: all test doctest-md stylecheck audit doc install examples clean distclean
|
||||||
|
|
||||||
all:
|
all:
|
||||||
cargo build --release
|
cargo build --release
|
||||||
@@ -20,6 +20,11 @@ test:
|
|||||||
doctest-md:
|
doctest-md:
|
||||||
./scripts/doctest-md.sh
|
./scripts/doctest-md.sh
|
||||||
|
|
||||||
|
# Mechanical style checks (tabs, attribute-bracket spacing) — the
|
||||||
|
# grep-verifiable subset of code_style_guide.md.
|
||||||
|
stylecheck:
|
||||||
|
./scripts/style-check.sh
|
||||||
|
|
||||||
audit:
|
audit:
|
||||||
@command -v cargo-audit >/dev/null 2>&1 || cargo install cargo-audit --locked
|
@command -v cargo-audit >/dev/null 2>&1 || cargo install cargo-audit --locked
|
||||||
cargo audit
|
cargo audit
|
||||||
|
|||||||
@@ -133,4 +133,4 @@ fn main()
|
|||||||
- The item brace styles (`brace_style`, `control_brace_style`) are **unstable, nightly-only** options.
|
- The item brace styles (`brace_style`, `control_brace_style`) are **unstable, nightly-only** options.
|
||||||
- Even on nightly, the combination of Allman opening braces with a compact `} else {` is not representable: `control_brace_style = "AlwaysNextLine"` also pushes `else` onto its own line.
|
- Even on nightly, the combination of Allman opening braces with a compact `} else {` is not representable: `control_brace_style = "AlwaysNextLine"` also pushes `else` onto its own line.
|
||||||
|
|
||||||
The repository ships a `rustfmt.toml` containing only `disable_all_formatting = true`, so an accidental `cargo fmt` — or an editor with format-on-save wired to rustfmt — is a no-op instead of a 40 000-line diff. Style is enforced by review, not by a formatter.
|
The repository ships a `rustfmt.toml` containing only `disable_all_formatting = true`, so an accidental `cargo fmt` — or an editor with format-on-save wired to rustfmt — is a no-op instead of a 40 000-line diff. The mechanically verifiable subset (tab indentation, attribute-bracket spacing) is enforced by `make stylecheck` (`scripts/style-check.sh`), which CI runs on every push; everything else — brace placement, paren spacing, naming — is enforced by review.
|
||||||
|
|||||||
@@ -583,12 +583,15 @@ viewport( panel_view )
|
|||||||
|
|
||||||
### `flex`
|
### `flex`
|
||||||
|
|
||||||
A row-only filler wrapper. Treats its non-spacer child like a
|
A filler wrapper for both flow layouts. Treats its non-spacer child
|
||||||
[`spacer`](#spacer) for leftover-width distribution but draws the child
|
like a [`spacer`](#spacer) for leftover-space distribution but draws
|
||||||
inside the allocated rect.
|
the child inside the allocated rect: leftover width inside a
|
||||||
|
[`row`](#row), leftover height inside a [`column`](#column). Weights
|
||||||
|
split the leftover proportionally between flex / spacer siblings.
|
||||||
|
|
||||||
**When**: a row where one non-trivial child should fill the remaining
|
**When**: a row where one non-trivial child should fill the remaining
|
||||||
width (a card next to a fixed-size icon).
|
width (a card next to a fixed-size icon), or a column where a child
|
||||||
|
should absorb the remaining height (a log pane under fixed toolbars).
|
||||||
|
|
||||||
```rust,no_run
|
```rust,no_run
|
||||||
# use ltk::{ column, flex, row, Element };
|
# use ltk::{ column, flex, row, Element };
|
||||||
@@ -998,6 +1001,14 @@ centred under the full rows above instead of left-aligned — useful for
|
|||||||
app switchers and gallery layouts where a 7-of-9 leftover band reads
|
app switchers and gallery layouts where a 7-of-9 leftover band reads
|
||||||
better balanced.
|
better balanced.
|
||||||
|
|
||||||
|
`grid_min_cell( width )` is the adaptive variant: instead of a fixed
|
||||||
|
column count, it fits as many columns as the available width allows
|
||||||
|
while keeping every cell at least `width` wide (never fewer than one),
|
||||||
|
re-deriving the count on every layout — so the same grid shows more
|
||||||
|
columns on a wide window and fewer on a phone. `width` accepts any
|
||||||
|
`Length`; `max_columns( n )` caps the derived count so cells grow
|
||||||
|
instead of multiplying on very wide surfaces.
|
||||||
|
|
||||||
### `spacer`
|
### `spacer`
|
||||||
|
|
||||||
An invisible flexible filler. Inside a column / row, absorbs leftover
|
An invisible flexible filler. Inside a column / row, absorbs leftover
|
||||||
|
|||||||
30
scripts/style-check.sh
Executable file
30
scripts/style-check.sh
Executable file
@@ -0,0 +1,30 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Mechanical checks for the Modified Allman style (code_style_guide.md).
|
||||||
|
# Only rules grep can verify without false positives are enforced here —
|
||||||
|
# brace placement and paren spacing stay a review concern. Run via
|
||||||
|
# `make stylecheck`; CI runs it on every push.
|
||||||
|
set -u
|
||||||
|
fail=0
|
||||||
|
files=$( find src examples tests benches -name '*.rs' )
|
||||||
|
|
||||||
|
# 1. Tabs for indentation: no source line may start with a space.
|
||||||
|
hits=$( grep -nE '^ ' $files /dev/null )
|
||||||
|
if [ -n "$hits" ]
|
||||||
|
then
|
||||||
|
printf '%s\n' "$hits"
|
||||||
|
echo "style: space indentation found — this tree indents with tabs"
|
||||||
|
fail=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 2. Spaces inside attribute brackets: #[ derive( Clone ) ], #[ test ],
|
||||||
|
# #![ deny( … ) ]. Comment lines are skipped so prose may mention raw
|
||||||
|
# attribute syntax like `#[doc(hidden)]`.
|
||||||
|
hits=$( grep -nE '#!?\[[a-zA-Z_]' $files /dev/null | grep -vE '^[^:]+:[0-9]+:[[:space:]]*(///|//!|//)' )
|
||||||
|
if [ -n "$hits" ]
|
||||||
|
then
|
||||||
|
printf '%s\n' "$hits"
|
||||||
|
echo "style: unspaced attribute brackets — write #[ derive( … ) ], #[ test ]"
|
||||||
|
fail=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit $fail
|
||||||
@@ -160,11 +160,14 @@ impl<Msg: Clone> Column<Msg>
|
|||||||
|
|
||||||
fn content_h( &self, inner_w: f32, canvas: &Canvas ) -> f32
|
fn content_h( &self, inner_w: f32, canvas: &Canvas ) -> f32
|
||||||
{
|
{
|
||||||
// Spacers contribute 0 to natural height; spacing still applies between all children.
|
// Spacers and flex children contribute 0 to natural height (their
|
||||||
|
// real height is leftover distribution, mirroring how a row counts
|
||||||
|
// flex width); spacing still applies between all children.
|
||||||
self.children.iter()
|
self.children.iter()
|
||||||
.map( |c| match c
|
.map( |c| match c
|
||||||
{
|
{
|
||||||
Element::Spacer( s ) => s.resolved_height( canvas ).unwrap_or( 0.0 ),
|
Element::Spacer( s ) => s.resolved_height( canvas ).unwrap_or( 0.0 ),
|
||||||
|
Element::Flex( _ ) => 0.0,
|
||||||
other => other.preferred_size( inner_w, canvas ).1,
|
other => other.preferred_size( inner_w, canvas ).1,
|
||||||
} )
|
} )
|
||||||
.sum::<f32>()
|
.sum::<f32>()
|
||||||
@@ -229,6 +232,7 @@ impl<Msg: Clone> Column<Msg>
|
|||||||
{
|
{
|
||||||
Element::Spacer( s ) if s.resolved_height( canvas ).is_none() => s.weight,
|
Element::Spacer( s ) if s.resolved_height( canvas ).is_none() => s.weight,
|
||||||
Element::Scroll( s ) if s.axis.allows_y() => 1,
|
Element::Scroll( s ) if s.axis.allows_y() => 1,
|
||||||
|
Element::Flex( f ) => f.weight,
|
||||||
_ => 0,
|
_ => 0,
|
||||||
} )
|
} )
|
||||||
.sum();
|
.sum();
|
||||||
@@ -237,6 +241,9 @@ impl<Msg: Clone> Column<Msg>
|
|||||||
.map( |c|
|
.map( |c|
|
||||||
{
|
{
|
||||||
if matches!( c, Element::Scroll( s ) if s.axis.allows_y() )
|
if matches!( c, Element::Scroll( s ) if s.axis.allows_y() )
|
||||||
|
{
|
||||||
|
0.0
|
||||||
|
} else if matches!( c, Element::Flex( _ ) )
|
||||||
{
|
{
|
||||||
0.0
|
0.0
|
||||||
} else if let Element::Spacer( s ) = c {
|
} else if let Element::Spacer( s ) = c {
|
||||||
@@ -251,7 +258,8 @@ impl<Msg: Clone> Column<Msg>
|
|||||||
let avail_h = rect.height - pad * 2.0;
|
let avail_h = rect.height - pad * 2.0;
|
||||||
let avail_spare = ( avail_h - fixed_h ).max( 0.0 );
|
let avail_spare = ( avail_h - fixed_h ).max( 0.0 );
|
||||||
|
|
||||||
// `center_y` only applies when there are no spacers.
|
// `center_y` only applies when there are no flexible children
|
||||||
|
// (weight-only spacers, y-scrolls, flex wrappers).
|
||||||
let start_y = if total_weight == 0 && self.center_y
|
let start_y = if total_weight == 0 && self.center_y
|
||||||
{
|
{
|
||||||
rect.y + pad + avail_spare / 2.0
|
rect.y + pad + avail_spare / 2.0
|
||||||
@@ -290,6 +298,16 @@ impl<Msg: Clone> Column<Msg>
|
|||||||
};
|
};
|
||||||
( inner_w, h )
|
( inner_w, h )
|
||||||
},
|
},
|
||||||
|
Element::Flex( f ) =>
|
||||||
|
{
|
||||||
|
let h = if total_weight > 0
|
||||||
|
{
|
||||||
|
avail_spare * f.weight as f32 / total_weight as f32
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
( inner_w, h )
|
||||||
|
},
|
||||||
other => other.preferred_size( inner_w, canvas ),
|
other => other.preferred_size( inner_w, canvas ),
|
||||||
};
|
};
|
||||||
let x = if self.align_center_x && !matches!( child, Element::Spacer( _ ) )
|
let x = if self.align_center_x && !matches!( child, Element::Spacer( _ ) )
|
||||||
@@ -438,4 +456,52 @@ mod tests
|
|||||||
let col = column::<()>().padding( 0.0 ).max_width( Length::vmin( 20.0 ) );
|
let col = column::<()>().padding( 0.0 ).max_width( Length::vmin( 20.0 ) );
|
||||||
assert_eq!( col.inner_w( 200.0, &canvas ), 120.0 );
|
assert_eq!( col.inner_w( 200.0, &canvas ), 120.0 );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[ test ]
|
||||||
|
fn flex_child_takes_leftover_height()
|
||||||
|
{
|
||||||
|
// A 30 px fixed spacer and one flex child in a 100 px rect:
|
||||||
|
// the flex gets the remaining 70 px at full inner width.
|
||||||
|
let canvas = make_canvas();
|
||||||
|
let col = column::<()>()
|
||||||
|
.padding( 0.0 )
|
||||||
|
.spacing( 0.0 )
|
||||||
|
.push( crate::spacer().height( 30.0 ) )
|
||||||
|
.push( crate::flex( crate::spacer() ) );
|
||||||
|
let rect = crate::types::Rect { x: 0.0, y: 0.0, width: 200.0, height: 100.0 };
|
||||||
|
let rects = col.layout( rect, &canvas );
|
||||||
|
assert_eq!( rects.len(), 2 );
|
||||||
|
assert!( ( rects[1].0.height - 70.0 ).abs() < 0.01 );
|
||||||
|
assert!( ( rects[1].0.width - 200.0 ).abs() < 0.01 );
|
||||||
|
}
|
||||||
|
|
||||||
|
#[ test ]
|
||||||
|
fn flex_children_split_leftover_by_weight()
|
||||||
|
{
|
||||||
|
// Two flex children weighted 1:3 share 100 px as 25 / 75.
|
||||||
|
let canvas = make_canvas();
|
||||||
|
let col = column::<()>()
|
||||||
|
.padding( 0.0 )
|
||||||
|
.spacing( 0.0 )
|
||||||
|
.push( crate::flex( crate::spacer() ) )
|
||||||
|
.push( crate::flex( crate::spacer() ).weight( 3 ) );
|
||||||
|
let rect = crate::types::Rect { x: 0.0, y: 0.0, width: 200.0, height: 100.0 };
|
||||||
|
let rects = col.layout( rect, &canvas );
|
||||||
|
assert!( ( rects[0].0.height - 25.0 ).abs() < 0.01 );
|
||||||
|
assert!( ( rects[1].0.height - 75.0 ).abs() < 0.01 );
|
||||||
|
}
|
||||||
|
|
||||||
|
#[ test ]
|
||||||
|
fn flex_contributes_zero_to_natural_height()
|
||||||
|
{
|
||||||
|
// Natural height counts only the fixed spacer, like row width math.
|
||||||
|
let canvas = make_canvas();
|
||||||
|
let col = column::<()>()
|
||||||
|
.padding( 0.0 )
|
||||||
|
.spacing( 0.0 )
|
||||||
|
.push( crate::spacer().height( 30.0 ) )
|
||||||
|
.push( crate::flex( crate::spacer() ) );
|
||||||
|
let ( _, h ) = col.preferred_size( 200.0, &canvas );
|
||||||
|
assert_eq!( h, 30.0 );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,8 +33,13 @@ pub struct WrapGrid<Msg: Clone>
|
|||||||
{
|
{
|
||||||
/// Child widgets laid out in row-major order.
|
/// Child widgets laid out in row-major order.
|
||||||
pub( crate ) children: Vec<Element<Msg>>,
|
pub( crate ) children: Vec<Element<Msg>>,
|
||||||
/// Number of columns per row.
|
/// Number of columns per row. Ignored when `min_cell_width` is set.
|
||||||
pub( crate ) columns: usize,
|
pub( crate ) columns: usize,
|
||||||
|
/// Adaptive mode: derive the column count from the available width
|
||||||
|
/// so every cell is at least this wide. See [`grid_min_cell`].
|
||||||
|
pub( crate ) min_cell_width: Option<Length>,
|
||||||
|
/// Upper bound on the derived column count in adaptive mode.
|
||||||
|
pub( crate ) max_columns: Option<usize>,
|
||||||
/// Horizontal gap between cells.
|
/// Horizontal gap between cells.
|
||||||
pub( crate ) spacing_x: Length,
|
pub( crate ) spacing_x: Length,
|
||||||
/// Vertical gap between rows.
|
/// Vertical gap between rows.
|
||||||
@@ -93,6 +98,36 @@ impl<Msg: Clone> WrapGrid<Msg>
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Cap the column count derived by [`grid_min_cell`] so cells stop
|
||||||
|
/// multiplying on very wide surfaces and grow instead. No effect on
|
||||||
|
/// a fixed-column [`grid`].
|
||||||
|
pub fn max_columns( mut self, n: usize ) -> Self
|
||||||
|
{
|
||||||
|
self.max_columns = Some( n );
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Column count for the given inner width: fixed, or derived from
|
||||||
|
/// `min_cell_width` (as many columns as fit at least that wide,
|
||||||
|
/// never fewer than one, capped by `max_columns`).
|
||||||
|
fn effective_columns( &self, inner_w: f32, sx: f32, canvas: &Canvas ) -> usize
|
||||||
|
{
|
||||||
|
match self.min_cell_width
|
||||||
|
{
|
||||||
|
Some( m ) =>
|
||||||
|
{
|
||||||
|
let m = m.resolve( canvas.viewport_layout(), Length::EM_BASE_DEFAULT ).max( 1.0 );
|
||||||
|
let cols = ( ( ( inner_w + sx ) / ( m + sx ) ).floor() as usize ).max( 1 );
|
||||||
|
match self.max_columns
|
||||||
|
{
|
||||||
|
Some( cap ) => cols.min( cap.max( 1 ) ),
|
||||||
|
None => cols,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => self.columns,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn resolved( &self, canvas: &Canvas ) -> ( f32, f32, f32 )
|
fn resolved( &self, canvas: &Canvas ) -> ( f32, f32, f32 )
|
||||||
{
|
{
|
||||||
let vp = canvas.viewport_layout();
|
let vp = canvas.viewport_layout();
|
||||||
@@ -107,13 +142,13 @@ impl<Msg: Clone> WrapGrid<Msg>
|
|||||||
/// Compute the preferred size given an available width.
|
/// Compute the preferred size given an available width.
|
||||||
pub fn preferred_size( &self, max_width: f32, canvas: &Canvas ) -> (f32, f32)
|
pub fn preferred_size( &self, max_width: f32, canvas: &Canvas ) -> (f32, f32)
|
||||||
{
|
{
|
||||||
if self.children.is_empty() || self.columns == 0
|
let ( sx, sy, pad ) = self.resolved( canvas );
|
||||||
|
let inner_w = (max_width - pad * 2.0).max( 0.0 );
|
||||||
|
let cols = self.effective_columns( inner_w, sx, canvas );
|
||||||
|
if self.children.is_empty() || cols == 0
|
||||||
{
|
{
|
||||||
return ( max_width, 0.0 );
|
return ( max_width, 0.0 );
|
||||||
}
|
}
|
||||||
let ( sx, sy, pad ) = self.resolved( canvas );
|
|
||||||
let cols = self.columns;
|
|
||||||
let inner_w = (max_width - pad * 2.0).max( 0.0 );
|
|
||||||
let cell_w = (inner_w - sx * (cols as f32 - 1.0)).max( 0.0 ) / cols as f32;
|
let cell_w = (inner_w - sx * (cols as f32 - 1.0)).max( 0.0 ) / cols as f32;
|
||||||
let row_count = (self.children.len() + cols - 1) / cols;
|
let row_count = (self.children.len() + cols - 1) / cols;
|
||||||
|
|
||||||
@@ -135,13 +170,13 @@ impl<Msg: Clone> WrapGrid<Msg>
|
|||||||
/// Compute child rects. Returns `(child_rect, index_in_children)` pairs.
|
/// Compute child rects. Returns `(child_rect, index_in_children)` pairs.
|
||||||
pub fn layout( &self, rect: Rect, canvas: &Canvas ) -> Vec<(Rect, usize)>
|
pub fn layout( &self, rect: Rect, canvas: &Canvas ) -> Vec<(Rect, usize)>
|
||||||
{
|
{
|
||||||
if self.children.is_empty() || self.columns == 0
|
let ( sx, sy, pad ) = self.resolved( canvas );
|
||||||
|
let inner_w = (rect.width - pad * 2.0).max( 0.0 );
|
||||||
|
let cols = self.effective_columns( inner_w, sx, canvas );
|
||||||
|
if self.children.is_empty() || cols == 0
|
||||||
{
|
{
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
}
|
}
|
||||||
let ( sx, sy, pad ) = self.resolved( canvas );
|
|
||||||
let cols = self.columns;
|
|
||||||
let inner_w = (rect.width - pad * 2.0).max( 0.0 );
|
|
||||||
let cell_w = (inner_w - sx * (cols as f32 - 1.0)).max( 0.0 ) / cols as f32;
|
let cell_w = (inner_w - sx * (cols as f32 - 1.0)).max( 0.0 ) / cols as f32;
|
||||||
let x0 = rect.x + pad;
|
let x0 = rect.x + pad;
|
||||||
let mut y = rect.y + pad;
|
let mut y = rect.y + pad;
|
||||||
@@ -185,6 +220,8 @@ impl<Msg: Clone> WrapGrid<Msg>
|
|||||||
{
|
{
|
||||||
children: self.children.into_iter().map( |c| c.map_arc( f ) ).collect(),
|
children: self.children.into_iter().map( |c| c.map_arc( f ) ).collect(),
|
||||||
columns: self.columns,
|
columns: self.columns,
|
||||||
|
min_cell_width: self.min_cell_width,
|
||||||
|
max_columns: self.max_columns,
|
||||||
spacing_x: self.spacing_x,
|
spacing_x: self.spacing_x,
|
||||||
spacing_y: self.spacing_y,
|
spacing_y: self.spacing_y,
|
||||||
padding: self.padding,
|
padding: self.padding,
|
||||||
@@ -390,6 +427,71 @@ mod tests
|
|||||||
let rects = g.layout( rect, &c );
|
let rects = g.layout( rect, &c );
|
||||||
assert!( rects[2].0.x.abs() < 0.01 );
|
assert!( rects[2].0.x.abs() < 0.01 );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- adaptive column count (grid_min_cell) ---
|
||||||
|
|
||||||
|
fn min_cell_grid( min: f32, n: usize, spacing: f32 ) -> WrapGrid<()>
|
||||||
|
{
|
||||||
|
let mut g = grid_min_cell( min ).spacing( spacing );
|
||||||
|
for _ in 0..n { g = g.push( spacer() ); }
|
||||||
|
g
|
||||||
|
}
|
||||||
|
|
||||||
|
#[ test ]
|
||||||
|
fn min_cell_derives_columns_from_width()
|
||||||
|
{
|
||||||
|
// 400px / min 90 => floor(400/90) = 4 columns, cells 100px.
|
||||||
|
let g = min_cell_grid( 90.0, 8, 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 );
|
||||||
|
for ( r, _ ) in &rects { assert!( ( r.width - 100.0 ).abs() < 0.01 ); }
|
||||||
|
// 8 children in 4 columns => index 3 sits in the last column and
|
||||||
|
// index 4 wraps back to x = 0 on the next row.
|
||||||
|
assert!( ( rects[3].0.x - 300.0 ).abs() < 0.01 );
|
||||||
|
assert!( rects[4].0.x.abs() < 0.01 );
|
||||||
|
}
|
||||||
|
|
||||||
|
#[ test ]
|
||||||
|
fn min_cell_accounts_for_spacing()
|
||||||
|
{
|
||||||
|
// With spacing 10: floor((300+10)/(90+10)) = 3 columns; the resulting
|
||||||
|
// cells ((300 - 2*10)/3 ≈ 93.3) still clear the 90px minimum.
|
||||||
|
let g = min_cell_grid( 90.0, 3, 10.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 );
|
||||||
|
assert!( rects.iter().all( |( r, _ )| r.width >= 90.0 ) );
|
||||||
|
assert!( ( rects[0].0.y - rects[2].0.y ).abs() < 0.01 );
|
||||||
|
}
|
||||||
|
|
||||||
|
#[ test ]
|
||||||
|
fn min_cell_never_below_one_column()
|
||||||
|
{
|
||||||
|
// Narrower than the minimum still lays out a single column:
|
||||||
|
// both children sit flush left at the full 80px width.
|
||||||
|
let g = min_cell_grid( 120.0, 2, 0.0 );
|
||||||
|
let c = canvas();
|
||||||
|
let rect = Rect { x: 0.0, y: 0.0, width: 80.0, height: 400.0 };
|
||||||
|
let rects = g.layout( rect, &c );
|
||||||
|
assert_eq!( rects.len(), 2 );
|
||||||
|
assert!( rects[1].0.x.abs() < 0.01 );
|
||||||
|
for ( r, _ ) in &rects { assert!( ( r.width - 80.0 ).abs() < 0.01 ); }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[ test ]
|
||||||
|
fn max_columns_caps_adaptive_count()
|
||||||
|
{
|
||||||
|
// 400px / min 90 would give 4; the cap keeps 2 and cells grow to 200.
|
||||||
|
let g = min_cell_grid( 90.0, 4, 0.0 ).max_columns( 2 );
|
||||||
|
let c = canvas();
|
||||||
|
let rect = Rect { x: 0.0, y: 0.0, width: 400.0, height: 400.0 };
|
||||||
|
let rects = g.layout( rect, &c );
|
||||||
|
for ( r, _ ) in &rects { assert!( ( r.width - 200.0 ).abs() < 0.01 ); }
|
||||||
|
// Two columns: index 1 fills the second column, index 2 wraps.
|
||||||
|
assert!( ( rects[1].0.x - 200.0 ).abs() < 0.01 );
|
||||||
|
assert!( rects[2].0.x.abs() < 0.01 );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a grid layout with the given number of columns.
|
/// Create a grid layout with the given number of columns.
|
||||||
@@ -410,6 +512,50 @@ pub fn grid<Msg: Clone>( columns: usize ) -> WrapGrid<Msg>
|
|||||||
{
|
{
|
||||||
children: Vec::new(),
|
children: Vec::new(),
|
||||||
columns,
|
columns,
|
||||||
|
min_cell_width: None,
|
||||||
|
max_columns: None,
|
||||||
|
spacing_x: Length::px( 8.0 ),
|
||||||
|
spacing_y: Length::px( 8.0 ),
|
||||||
|
padding: Length::px( 0.0 ),
|
||||||
|
centre_last_row: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create an adaptive grid: the column count is derived at layout time
|
||||||
|
/// from the available width, fitting as many columns as possible while
|
||||||
|
/// keeping every cell at least `min_cell_width` wide (never fewer than
|
||||||
|
/// one). Cells then share the width equally, so they range between
|
||||||
|
/// `min_cell_width` and just under twice it — cap the count with
|
||||||
|
/// [`max_columns`](WrapGrid::max_columns) to let cells grow instead on
|
||||||
|
/// very wide surfaces.
|
||||||
|
///
|
||||||
|
/// Accepts logical `f32` pixels or any [`Length`] (e.g.
|
||||||
|
/// `Length::fluid( 96.0 )` for a threshold that scales with the
|
||||||
|
/// surface).
|
||||||
|
///
|
||||||
|
/// ```rust,no_run
|
||||||
|
/// # use std::sync::Arc;
|
||||||
|
/// # use ltk::{ grid_min_cell, icon_button, scroll, Element };
|
||||||
|
/// # #[ derive( Clone ) ] enum Msg { Open( usize ) }
|
||||||
|
/// # fn _ex( data: Arc<Vec<u8>>, w: u32, h: u32 ) -> Element<Msg> {
|
||||||
|
/// scroll(
|
||||||
|
/// grid_min_cell( 96.0 )
|
||||||
|
/// .max_columns( 8 )
|
||||||
|
/// .spacing( 12.0 )
|
||||||
|
/// .push( icon_button( data, w, h ).on_press( Msg::Open( 0 ) ) )
|
||||||
|
/// // ...
|
||||||
|
/// )
|
||||||
|
/// .into()
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
|
pub fn grid_min_cell<Msg: Clone>( min_cell_width: impl Into<Length> ) -> WrapGrid<Msg>
|
||||||
|
{
|
||||||
|
WrapGrid
|
||||||
|
{
|
||||||
|
children: Vec::new(),
|
||||||
|
columns: 0,
|
||||||
|
min_cell_width: Some( min_cell_width.into() ),
|
||||||
|
max_columns: None,
|
||||||
spacing_x: Length::px( 8.0 ),
|
spacing_x: Length::px( 8.0 ),
|
||||||
spacing_y: Length::px( 8.0 ),
|
spacing_y: Length::px( 8.0 ),
|
||||||
padding: Length::px( 0.0 ),
|
padding: Length::px( 0.0 ),
|
||||||
|
|||||||
@@ -101,7 +101,8 @@
|
|||||||
//! - [`column()`] — vertical flow.
|
//! - [`column()`] — vertical flow.
|
||||||
//! - [`row()`] — horizontal flow.
|
//! - [`row()`] — horizontal flow.
|
||||||
//! - [`stack()`] — z-order overlay with per-child alignment.
|
//! - [`stack()`] — z-order overlay with per-child alignment.
|
||||||
//! - [`grid()`] — fixed-column-count wrapping grid.
|
//! - [`grid()`] — fixed-column-count wrapping grid; [`grid_min_cell()`]
|
||||||
|
//! derives the column count from the width instead.
|
||||||
//! - [`spacer()`] — invisible flexible filler.
|
//! - [`spacer()`] — invisible flexible filler.
|
||||||
//!
|
//!
|
||||||
//! See [`layouts`] for the grouped landing page.
|
//! See [`layouts`] for the grouped landing page.
|
||||||
@@ -380,7 +381,7 @@ pub use layout::column::{ Column, column };
|
|||||||
pub use layout::row::{ Row, row };
|
pub use layout::row::{ Row, row };
|
||||||
pub use layout::stack::{ Stack, stack, HAlign, VAlign };
|
pub use layout::stack::{ Stack, stack, HAlign, VAlign };
|
||||||
// push_aligned_margin is available as a method on Stack — no separate re-export needed.
|
// push_aligned_margin is available as a method on Stack — no separate re-export needed.
|
||||||
pub use layout::wrap_grid::{ WrapGrid, grid };
|
pub use layout::wrap_grid::{ WrapGrid, grid, grid_min_cell };
|
||||||
pub use widget::scroll::{ scroll, ScrollAxis };
|
pub use widget::scroll::{ scroll, ScrollAxis };
|
||||||
pub use widget::viewport::{ Viewport, viewport };
|
pub use widget::viewport::{ Viewport, viewport };
|
||||||
pub use widget::carousel::{ Carousel, carousel };
|
pub use widget::carousel::{ Carousel, carousel };
|
||||||
@@ -458,7 +459,7 @@ pub mod layouts
|
|||||||
Column, column,
|
Column, column,
|
||||||
Row, row,
|
Row, row,
|
||||||
Stack, stack, HAlign, VAlign,
|
Stack, stack, HAlign, VAlign,
|
||||||
WrapGrid, grid,
|
WrapGrid, grid, grid_min_cell,
|
||||||
Spacer, spacer,
|
Spacer, spacer,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -490,7 +491,7 @@ pub mod window
|
|||||||
button, icon_button, text, text_edit, img_widget,
|
button, icon_button, text, text_edit, img_widget,
|
||||||
container, checkbox, radio, toggle, separator,
|
container, checkbox, radio, toggle, separator,
|
||||||
progress_bar, list_item, slider, vslider, scroll, viewport,
|
progress_bar, list_item, slider, vslider, scroll, viewport,
|
||||||
column, row, stack, grid, spacer,
|
column, row, stack, grid, grid_min_cell, spacer,
|
||||||
TextAlign, SliderAxis,
|
TextAlign, SliderAxis,
|
||||||
run,
|
run,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,16 +4,19 @@
|
|||||||
use crate::render::Canvas;
|
use crate::render::Canvas;
|
||||||
use super::Element;
|
use super::Element;
|
||||||
|
|
||||||
/// Wraps an [`Element`] so that a [`Row`](crate::layout::row::Row) treats it
|
/// Wraps an [`Element`] so its parent treats it like a
|
||||||
/// like a [`Spacer`](crate::layout::spacer::Spacer) for leftover-width
|
/// [`Spacer`](crate::layout::spacer::Spacer) for leftover-space
|
||||||
/// distribution, but draws the child inside the allocated rect.
|
/// 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
|
/// Use this to make a non-trivial child fill remaining space without
|
||||||
/// to a hard-coded `max_width`. The row computes how much width is left after
|
/// resorting to a hard-coded size. The parent computes how much of its
|
||||||
/// the fixed-size siblings, splits it across all flex / spacer children
|
/// main axis is left after the fixed-size siblings, splits it across all
|
||||||
/// proportionally to their `weight`, and gives the flex its share. The wrapped
|
/// flex / spacer children proportionally to their `weight`, and gives
|
||||||
/// child sees that share as its layout rect — text inside it triggers its own
|
/// the flex its share. The wrapped child sees that share as its layout
|
||||||
/// elide path on overflow, columns adjust their inner width, etc.
|
/// rect — text inside it triggers its own elide path on overflow,
|
||||||
|
/// columns adjust their inner width, etc.
|
||||||
///
|
///
|
||||||
/// ```rust,no_run
|
/// ```rust,no_run
|
||||||
/// # use ltk::{ column, flex, row, Element };
|
/// # 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 struct Flex<Msg: Clone>
|
||||||
{
|
{
|
||||||
pub( crate ) child: Box<Element<Msg>>,
|
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
|
/// Relative weight when sharing leftover space with other flex / spacer
|
||||||
/// children in the same row (default `1`). A flex with `weight = 2`
|
/// children in the same row or column (default `1`). A flex with
|
||||||
/// claims twice the space of a sibling with `weight = 1`.
|
/// `weight = 2` claims twice the space of a sibling with `weight = 1`.
|
||||||
pub fn weight( mut self, w: u32 ) -> Self
|
pub fn weight( mut self, w: u32 ) -> Self
|
||||||
{
|
{
|
||||||
self.weight = w;
|
self.weight = w;
|
||||||
|
|||||||
Reference in New Issue
Block a user