From a7f953ca4283c4d18924fdb591276493334714db Mon Sep 17 00:00:00 2001 From: "Pedro M. de Echanove Pasquin" Date: Thu, 30 Jul 2026 22:20:21 +0200 Subject: [PATCH] =?UTF-8?q?layout:=20adaptive=20grid=20and=20vertical=20fl?= =?UTF-8?q?ex;=20enforce=20the=20mechanical=20style=20rules=20grid=5Fmin?= =?UTF-8?q?=5Fcell(=20width=20)=20adds=20the=20adaptive=20mode=20WrapGrid?= =?UTF-8?q?=20was=20missing:=20instead=20of=20a=20fixed=20column=20count,?= =?UTF-8?q?=20the=20count=20is=20derived=20at=20layout=20time=20from=20the?= =?UTF-8?q?=20available=20width=20so=20every=20cell=20is=20at=20least=20`w?= =?UTF-8?q?idth`=20wide=20(any=20Length,=20so=20a=20fluid=20threshold=20wo?= =?UTF-8?q?rks;=20never=20fewer=20than=20one=20column),=20re-derived=20on?= =?UTF-8?q?=20every=20resize.=20Cells=20then=20share=20the=20width=20equal?= =?UTF-8?q?ly,=20and=20WrapGrid::max=5Fcolumns(=20n=20)=20caps=20the=20der?= =?UTF-8?q?ived=20count=20so=20cells=20grow=20instead=20of=20multiplying?= =?UTF-8?q?=20on=20very=20wide=20surfaces=20=E2=80=94=20the=20cap=20only?= =?UTF-8?q?=20applies=20to=20the=20adaptive=20mode,=20grid(=20n=20)=20keep?= =?UTF-8?q?s=20the=20fixed=20behaviour=20untouched.=20Four=20new=20layout?= =?UTF-8?q?=20tests=20cover=20width=20derivation,=20spacing=20accounting,?= =?UTF-8?q?=20the=20one-column=20floor=20and=20the=20cap;=20the=20row-wrap?= =?UTF-8?q?=20assertions=20check=20X=20positions=20rather=20than=20Y=20bec?= =?UTF-8?q?ause=20zero-height=20spacer=20children=20produce=20zero-height?= =?UTF-8?q?=20rows.=20flex(=20child=20)=20now=20distributes=20leftover=20h?= =?UTF-8?q?eight=20inside=20a=20Column,=20mirroring=20its=20leftover-width?= =?UTF-8?q?=20behaviour=20in=20a=20Row:=20flex=20children=20join=20the=20w?= =?UTF-8?q?eight=20pool=20alongside=20weight-only=20spacers=20and=20y-scro?= =?UTF-8?q?lls,=20draw=20their=20child=20inside=20the=20allocated=20share?= =?UTF-8?q?=20at=20full=20inner=20width,=20and=20contribute=20zero=20to=20?= =?UTF-8?q?the=20column's=20natural=20height=20exactly=20like=20a=20row=20?= =?UTF-8?q?counts=20flex=20width.=20This=20closes=20the=20documented=20"ve?= =?UTF-8?q?rtical=20flex=20is=20not=20yet=20implemented"=20gap;=20the=20Fl?= =?UTF-8?q?ex=20rustdoc=20and=20the=20widgets.md=20entry=20now=20describe?= =?UTF-8?q?=20both=20axes.=20Add=20scripts/style-check.sh=20and=20a=20make?= =?UTF-8?q?=20stylecheck=20target,=20wired=20into=20CI:=20mechanical=20ver?= =?UTF-8?q?ification=20of=20the=20grep-checkable=20subset=20of=20code=5Fst?= =?UTF-8?q?yle=5Fguide.md=20=E2=80=94=20tab=20indentation=20and=20spaces?= =?UTF-8?q?=20inside=20attribute=20brackets=20=E2=80=94=20with=20comment?= =?UTF-8?q?=20lines=20skipped=20so=20prose=20may=20cite=20raw=20attribute?= =?UTF-8?q?=20syntax.=20To=20turn=20the=20check=20on=20green,=20the=2082?= =?UTF-8?q?=20unspaced=20attribute=20sites=20across=2024=20files=20(#[test?= =?UTF-8?q?],=20#[derive(...)],=20#[cfg(...)],=20#[allow(...)])=20were=20n?= =?UTF-8?q?ormalized=20to=20the=20spaced=20form.=20CONTRIBUTING=20and=20th?= =?UTF-8?q?e=20style=20guide=20reference=20the=20new=20target;=20brace=20p?= =?UTF-8?q?lacement=20and=20paren=20spacing=20remain=20review=20concerns.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/ci.yml | 3 + CHANGELOG.md | 3 + CONTRIBUTING.md | 1 + Makefile | 7 +- code_style_guide.md | 2 +- docs/widgets.md | 19 ++- examples/widgets.rs | 2 +- scripts/style-check.sh | 30 +++++ src/app.rs | 16 +-- src/event_loop/app_data.rs | 6 +- src/event_loop/surface.rs | 6 +- src/event_loop/tooltip.rs | 4 +- src/layout/column.rs | 70 ++++++++++- src/layout/wrap_grid.rs | 194 +++++++++++++++++++++++++++---- src/lib.rs | 11 +- src/widget/carousel/mod.rs | 2 +- src/widget/carousel/tests.rs | 18 +-- src/widget/checkbox/mod.rs | 2 +- src/widget/checkbox/tests.rs | 4 +- src/widget/flex/mod.rs | 31 +++-- src/widget/list_item/mod.rs | 2 +- src/widget/list_item/tests.rs | 2 +- src/widget/progress_bar/mod.rs | 2 +- src/widget/progress_bar/tests.rs | 4 +- src/widget/radio/mod.rs | 2 +- src/widget/radio/tests.rs | 4 +- src/widget/scroll/mod.rs | 2 +- src/widget/scroll/tests.rs | 14 +-- src/widget/separator/mod.rs | 2 +- src/widget/separator/tests.rs | 6 +- src/widget/slider/tests.rs | 18 +-- src/widget/toggle/mod.rs | 2 +- src/widget/toggle/tests.rs | 4 +- 33 files changed, 380 insertions(+), 115 deletions(-) create mode 100755 scripts/style-check.sh diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index e3d4c30..cfff65f 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -71,6 +71,9 @@ jobs: - name: Markdown doctests run: ./scripts/doctest-md.sh + - name: Style check + run: ./scripts/style-check.sh + audit: name: cargo audit runs-on: ubuntu-latest diff --git a/CHANGELOG.md b/CHANGELOG.md index 653ed84..341d6c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. - **`Button::font_size` / `height` / `width`** and **`TextEdit::height`** builders, all `impl Into`, 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`. +- **`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. - **`test-support` Cargo feature** gates the `test_support` module so third-party builds never see it (ltk's own `make test` enables it). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 622f5cc..5a57fc1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -50,6 +50,7 @@ The `Makefile` wraps the common targets: make all # cargo build --release make test # cargo test --features test-support 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 doc # cargo doc --no-deps make examples # run every example under examples/ in turn diff --git a/Makefile b/Makefile index fc2cedc..f19f9b2 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ DOCDIR ?= /usr/share/doc/libltk-doc/html # ignore filesystem entries with the same name — without this `examples` # silently no-ops because the `examples/` directory exists, and `doc` # 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: cargo build --release @@ -20,6 +20,11 @@ test: doctest-md: ./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: @command -v cargo-audit >/dev/null 2>&1 || cargo install cargo-audit --locked cargo audit diff --git a/code_style_guide.md b/code_style_guide.md index 4e93f86..bfb57f5 100755 --- a/code_style_guide.md +++ b/code_style_guide.md @@ -133,4 +133,4 @@ fn main() - 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. -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. diff --git a/docs/widgets.md b/docs/widgets.md index 886cf82..834d0b2 100644 --- a/docs/widgets.md +++ b/docs/widgets.md @@ -583,12 +583,15 @@ viewport( panel_view ) ### `flex` -A row-only filler wrapper. Treats its non-spacer child like a -[`spacer`](#spacer) for leftover-width distribution but draws the child -inside the allocated rect. +A filler wrapper for both flow layouts. Treats its non-spacer child +like a [`spacer`](#spacer) for leftover-space distribution but draws +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 -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 # 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 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` An invisible flexible filler. Inside a column / row, absorbs leftover diff --git a/examples/widgets.rs b/examples/widgets.rs index ddd532f..509ea6b 100644 --- a/examples/widgets.rs +++ b/examples/widgets.rs @@ -7,7 +7,7 @@ use ltk::{ spinner, tabs, }; -#[derive( Clone )] +#[ derive( Clone ) ] enum Msg { ToggleWifi, diff --git a/scripts/style-check.sh b/scripts/style-check.sh new file mode 100755 index 0000000..b28e1de --- /dev/null +++ b/scripts/style-check.sh @@ -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 diff --git a/src/app.rs b/src/app.rs index 6857207..392c1a8 100644 --- a/src/app.rs +++ b/src/app.rs @@ -23,7 +23,7 @@ pub enum ToplevelEvent } /// Wayland shell mode for the application surface. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[ derive( Debug, Clone, Copy, PartialEq, Eq ) ] pub enum ShellMode { /// Normal application window using xdg-shell protocol. @@ -41,7 +41,7 @@ pub enum ShellMode } /// Layer-shell layer position. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[ derive( Debug, Clone, Copy, PartialEq, Eq ) ] pub enum Layer { /// Below normal windows (wallpapers, desktop backgrounds). @@ -72,7 +72,7 @@ impl Layer /// Layer-shell anchor edges. /// Determines which screen edges the surface is attached to. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[ derive( Debug, Clone, Copy, PartialEq, Eq ) ] pub struct Anchor { pub top: bool, @@ -115,13 +115,13 @@ impl Anchor /// overlay list between frames: if the same `OverlayId` is returned from /// [`App::overlays`] on consecutive frames the underlying Wayland surface /// and its internal state are kept; if it disappears the surface is destroyed. -#[derive( Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord )] +#[ derive( Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord ) ] pub struct OverlayId( pub u32 ); /// Stable identifier for a subsurface, used to diff the list returned by /// [`App::subsurfaces`] between frames the same way [`OverlayId`] diffs /// overlays. -#[derive( Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord )] +#[ derive( Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord ) ] pub struct SubsurfaceId( pub u32 ); /// Which surface a [`SubsurfaceSpec`] is composited as a child of. `Main` @@ -129,7 +129,7 @@ pub struct SubsurfaceId( pub u32 ); /// parents to one of the [`App::overlays`] surfaces, so a slide can ride /// above app windows the way an overlay panel does. An `Overlay` parent that /// is absent or not yet configured is skipped for that frame. -#[derive( Debug, Clone, Copy, PartialEq, Eq )] +#[ derive( Debug, Clone, Copy, PartialEq, Eq ) ] pub enum SubsurfaceParent { Main, @@ -138,7 +138,7 @@ pub enum SubsurfaceParent /// One of the surfaces an [`App`] can target with an invalidation. Used inside /// [`InvalidationScope::Only`] to name the affected surfaces. -#[derive( Debug, Clone, Copy, PartialEq, Eq, Hash )] +#[ derive( Debug, Clone, Copy, PartialEq, Eq, Hash ) ] pub enum SurfaceTarget { /// The application's main surface (the one returned by [`App::view`]). @@ -153,7 +153,7 @@ pub enum SurfaceTarget /// to let the runtime skip redraws on surfaces whose contents could not /// possibly have changed by the message in question. On a shell with many /// overlays most messages only touch one of them, so the savings are large. -#[derive( Debug, Clone )] +#[ derive( Debug, Clone ) ] pub enum InvalidationScope { /// Treat every surface as potentially affected (safe default). diff --git a/src/event_loop/app_data.rs b/src/event_loop/app_data.rs index 26af6bb..084f88f 100644 --- a/src/event_loop/app_data.rs +++ b/src/event_loop/app_data.rs @@ -56,7 +56,7 @@ pub struct AppData /// failed to initialise or `LTK_FORCE_SOFTWARE=1` — every surface then /// falls back to the SHM path. pub egl_context: Option>, - #[allow(dead_code)] + #[ allow( dead_code ) ] pub xdg_shell: Option, /// Shared layer-shell binding used for the main surface and every /// overlay. `None` when the compositor does not advertise the protocol. @@ -289,7 +289,7 @@ impl AppData /// refers to an overlay that is not currently registered — callers must /// only pass focus values obtained from [`focus_for_surface`] or from the /// per-device focus fields, which the run loop keeps in sync. - #[allow( dead_code )] + #[ allow( dead_code ) ] pub( crate ) fn surface( &self, focus: SurfaceFocus ) -> &SurfaceState { match focus @@ -313,7 +313,7 @@ impl AppData } /// Mutable counterpart of [`surface`]. - #[allow( dead_code )] + #[ allow( dead_code ) ] pub( crate ) fn surface_mut( &mut self, focus: SurfaceFocus ) -> &mut SurfaceState { match focus diff --git a/src/event_loop/surface.rs b/src/event_loop/surface.rs index 0b41dbe..b49bd6f 100644 --- a/src/event_loop/surface.rs +++ b/src/event_loop/surface.rs @@ -34,18 +34,18 @@ use crate::widget::LaidOutWidget; /// `Main` refers to the application's main surface (xdg window or layer /// shell). `Overlay( id )` refers to an auxiliary layer-shell surface created /// from an entry in [`crate::app::App::overlays`]. -#[derive( Debug, Clone, Copy, PartialEq, Eq, Hash )] +#[ derive( Debug, Clone, Copy, PartialEq, Eq, Hash ) ] pub( crate ) enum SurfaceFocus { Main, - #[allow( dead_code )] + #[ allow( dead_code ) ] Overlay( OverlayId ), } /// Configuration for a layer-shell surface, used both for the main surface /// (when the app uses [`crate::app::ShellMode::Layer`]) and for each overlay /// returned by [`crate::app::App::overlays`]. -#[derive( Clone )] +#[ derive( Clone ) ] pub( crate ) struct LayerConfig { pub layer: Layer, diff --git a/src/event_loop/tooltip.rs b/src/event_loop/tooltip.rs index d301a59..e9fe83d 100644 --- a/src/event_loop/tooltip.rs +++ b/src/event_loop/tooltip.rs @@ -8,7 +8,7 @@ use crate::types::{ Length, Rect }; pub const TOOLTIP_DELAY: std::time::Duration = std::time::Duration::from_millis( 600 ); -#[derive( Clone )] +#[ derive( Clone ) ] pub struct TooltipPending { pub focus: SurfaceFocus, @@ -18,7 +18,7 @@ pub struct TooltipPending pub anchor: Rect, } -#[derive( Clone )] +#[ derive( Clone ) ] pub struct TooltipVisible { pub focus: SurfaceFocus, diff --git a/src/layout/column.rs b/src/layout/column.rs index a305a0a..6718eb6 100644 --- a/src/layout/column.rs +++ b/src/layout/column.rs @@ -160,11 +160,14 @@ impl Column 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() .map( |c| match c { Element::Spacer( s ) => s.resolved_height( canvas ).unwrap_or( 0.0 ), + Element::Flex( _ ) => 0.0, other => other.preferred_size( inner_w, canvas ).1, } ) .sum::() @@ -229,6 +232,7 @@ impl Column { Element::Spacer( s ) if s.resolved_height( canvas ).is_none() => s.weight, Element::Scroll( s ) if s.axis.allows_y() => 1, + Element::Flex( f ) => f.weight, _ => 0, } ) .sum(); @@ -237,6 +241,9 @@ impl Column .map( |c| { if matches!( c, Element::Scroll( s ) if s.axis.allows_y() ) + { + 0.0 + } else if matches!( c, Element::Flex( _ ) ) { 0.0 } else if let Element::Spacer( s ) = c { @@ -251,7 +258,8 @@ impl Column let avail_h = rect.height - pad * 2.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 { rect.y + pad + avail_spare / 2.0 @@ -290,6 +298,16 @@ impl Column }; ( 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 ), }; 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 ) ); 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 ); + } } diff --git a/src/layout/wrap_grid.rs b/src/layout/wrap_grid.rs index 1ec299d..01b3fb8 100644 --- a/src/layout/wrap_grid.rs +++ b/src/layout/wrap_grid.rs @@ -33,8 +33,13 @@ pub struct WrapGrid { /// Child widgets laid out in row-major order. pub( crate ) children: Vec>, - /// Number of columns per row. + /// Number of columns per row. Ignored when `min_cell_width` is set. 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, + /// Upper bound on the derived column count in adaptive mode. + pub( crate ) max_columns: Option, /// Horizontal gap between cells. pub( crate ) spacing_x: Length, /// Vertical gap between rows. @@ -93,6 +98,36 @@ impl WrapGrid 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 ) { let vp = canvas.viewport_layout(); @@ -107,13 +142,13 @@ impl WrapGrid /// 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 + 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 ); } - 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 row_count = (self.children.len() + cols - 1) / cols; @@ -135,13 +170,13 @@ impl WrapGrid /// 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 + 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(); } - 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 x0 = rect.x + pad; let mut y = rect.y + pad; @@ -185,6 +220,8 @@ impl WrapGrid { children: self.children.into_iter().map( |c| c.map_arc( f ) ).collect(), columns: self.columns, + min_cell_width: self.min_cell_width, + max_columns: self.max_columns, spacing_x: self.spacing_x, spacing_y: self.spacing_y, padding: self.padding, @@ -201,7 +238,7 @@ impl From> for Element } } -#[cfg(test)] +#[ cfg( test ) ] mod tests { use super::*; @@ -221,7 +258,7 @@ mod tests // --- preferred_size --- - #[test] + #[ test ] fn empty_grid_height_is_zero() { let g: WrapGrid<()> = grid( 4 ); @@ -229,7 +266,7 @@ mod tests assert_eq!( h, 0.0 ); } - #[test] + #[ test ] fn preferred_width_equals_max_width() { let g = spacer_grid( 4, 8, 0.0, 0.0 ); @@ -239,7 +276,7 @@ mod tests // --- layout: cell widths --- - #[test] + #[ test ] fn cell_width_no_spacing_no_padding() { // 400px / 4 cols = 100px each @@ -251,7 +288,7 @@ mod tests for ( r, _ ) in &rects { assert!( (r.width - 100.0).abs() < 0.01 ); } } - #[test] + #[ test ] fn cell_width_with_spacing() { // (400 - 3 * 10) / 4 = 370 / 4 = 92.5 @@ -262,7 +299,7 @@ mod tests for ( r, _ ) in &rects { assert!( (r.width - 92.5).abs() < 0.01 ); } } - #[test] + #[ test ] fn cell_width_with_padding() { // inner = 400 - 2*20 = 360; 360 / 4 = 90 @@ -275,7 +312,7 @@ mod tests // --- layout: child count and indices --- - #[test] + #[ test ] fn layout_yields_one_rect_per_child() { let g = spacer_grid( 4, 7, 0.0, 0.0 ); @@ -285,7 +322,7 @@ mod tests assert_eq!( rects.len(), 7 ); } - #[test] + #[ test ] fn layout_indices_are_sequential() { let g = spacer_grid( 3, 5, 0.0, 0.0 ); @@ -298,7 +335,7 @@ mod tests // --- layout: column x-positions --- - #[test] + #[ test ] fn column_x_positions_no_spacing() { // 300px / 3 cols = 100px each, starting at x=0 @@ -312,7 +349,7 @@ mod tests assert!( (xs[2] - 200.0).abs() < 0.01 ); } - #[test] + #[ 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 @@ -329,7 +366,7 @@ mod tests // --- layout: partial last row --- - #[test] + #[ test ] fn partial_last_row_has_correct_count() { // 7 children, 4 cols => row 0: 4, row 1: 3. @@ -343,7 +380,7 @@ mod tests // --- layout: rect origin offset --- - #[test] + #[ test ] fn layout_respects_rect_origin() { let g = spacer_grid( 2, 2, 0.0, 0.0 ); @@ -356,7 +393,7 @@ mod tests // --- layout: centre_last_row --- - #[test] + #[ test ] fn last_row_centred_when_partial() { // 3 children, 2 cols => row 0: 2 items, row 1: 1 item centred. @@ -368,7 +405,7 @@ mod tests assert!( (rects[2].0.x - 50.0).abs() < 0.01 ); } - #[test] + #[ test ] fn centre_last_row_noop_on_full_row() { // 4 children, 2 cols => both rows full; nothing to centre. @@ -380,7 +417,7 @@ mod tests assert!( (rects[3].0.x - 100.0).abs() < 0.01 ); } - #[test] + #[ test ] fn centre_last_row_off_by_default() { // Same case as above but without the flag — last item stays at x=0. @@ -390,6 +427,71 @@ mod tests let rects = g.layout( rect, &c ); 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. @@ -410,6 +512,50 @@ pub fn grid( columns: usize ) -> WrapGrid { children: Vec::new(), 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>, w: u32, h: u32 ) -> Element { +/// 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( min_cell_width: impl Into ) -> WrapGrid +{ + WrapGrid + { + children: Vec::new(), + columns: 0, + min_cell_width: Some( min_cell_width.into() ), + max_columns: None, spacing_x: Length::px( 8.0 ), spacing_y: Length::px( 8.0 ), padding: Length::px( 0.0 ), diff --git a/src/lib.rs b/src/lib.rs index 99935db..2939f6d 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -33,7 +33,7 @@ //! ```rust,no_run //! use ltk::{App, Element, column, text, button, spacer, Color, ButtonVariant}; //! -//! #[derive(Clone)] +//! #[ derive( Clone ) ] //! enum Msg { Quit } //! //! struct MyApp; @@ -101,7 +101,8 @@ //! - [`column()`] — vertical flow. //! - [`row()`] — horizontal flow. //! - [`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. //! //! 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::stack::{ Stack, stack, HAlign, VAlign }; // 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::viewport::{ Viewport, viewport }; pub use widget::carousel::{ Carousel, carousel }; @@ -458,7 +459,7 @@ pub mod layouts Column, column, Row, row, Stack, stack, HAlign, VAlign, - WrapGrid, grid, + WrapGrid, grid, grid_min_cell, Spacer, spacer, }; } @@ -490,7 +491,7 @@ pub mod window button, icon_button, text, text_edit, img_widget, container, checkbox, radio, toggle, separator, progress_bar, list_item, slider, vslider, scroll, viewport, - column, row, stack, grid, spacer, + column, row, stack, grid, grid_min_cell, spacer, TextAlign, SliderAxis, run, }; diff --git a/src/widget/carousel/mod.rs b/src/widget/carousel/mod.rs index 111d635..92e9120 100644 --- a/src/widget/carousel/mod.rs +++ b/src/widget/carousel/mod.rs @@ -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 diff --git a/src/widget/carousel/tests.rs b/src/widget/carousel/tests.rs index 19d2893..b593b57 100644 --- a/src/widget/carousel/tests.rs +++ b/src/widget/carousel/tests.rs @@ -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(); diff --git a/src/widget/checkbox/mod.rs b/src/widget/checkbox/mod.rs index 9881494..50e55ce 100644 --- a/src/widget/checkbox/mod.rs +++ b/src/widget/checkbox/mod.rs @@ -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. diff --git a/src/widget/checkbox/tests.rs b/src/widget/checkbox/tests.rs index 16619f4..5e3bae5 100644 --- a/src/widget/checkbox/tests.rs +++ b/src/widget/checkbox/tests.rs @@ -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 ); diff --git a/src/widget/flex/mod.rs b/src/widget/flex/mod.rs index c46c6cc..f2c1b86 100644 --- a/src/widget/flex/mod.rs +++ b/src/widget/flex/mod.rs @@ -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 { pub( crate ) child: Box>, @@ -51,9 +50,9 @@ impl Flex } } - /// 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; diff --git a/src/widget/list_item/mod.rs b/src/widget/list_item/mod.rs index d510a9e..5f0b457 100644 --- a/src/widget/list_item/mod.rs +++ b/src/widget/list_item/mod.rs @@ -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 diff --git a/src/widget/list_item/tests.rs b/src/widget/list_item/tests.rs index 611dd94..834a64f 100644 --- a/src/widget/list_item/tests.rs +++ b/src/widget/list_item/tests.rs @@ -3,7 +3,7 @@ use super::*; -#[test] +#[ test ] fn list_item_default() { let l = list_item::<()>( "Test" ); diff --git a/src/widget/progress_bar/mod.rs b/src/widget/progress_bar/mod.rs index 37be94f..9fcd577 100644 --- a/src/widget/progress_bar/mod.rs +++ b/src/widget/progress_bar/mod.rs @@ -7,7 +7,7 @@ use super::Element; mod theme; -#[cfg(test)] +#[ cfg( test ) ] mod tests; /// A linear progress indicator for determinate operations. diff --git a/src/widget/progress_bar/tests.rs b/src/widget/progress_bar/tests.rs index 1b30ab8..9147bdf 100644 --- a/src/widget/progress_bar/tests.rs +++ b/src/widget/progress_bar/tests.rs @@ -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 ); diff --git a/src/widget/radio/mod.rs b/src/widget/radio/mod.rs index c786b1e..4a3002b 100644 --- a/src/widget/radio/mod.rs +++ b/src/widget/radio/mod.rs @@ -7,7 +7,7 @@ use super::Element; mod theme; -#[cfg(test)] +#[ cfg( test ) ] mod tests; /// One option inside a mutually-exclusive group. diff --git a/src/widget/radio/tests.rs b/src/widget/radio/tests.rs index a260e39..d253000 100644 --- a/src/widget/radio/tests.rs +++ b/src/widget/radio/tests.rs @@ -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 ); diff --git a/src/widget/scroll/mod.rs b/src/widget/scroll/mod.rs index 2f00fd8..c502571 100644 --- a/src/widget/scroll/mod.rs +++ b/src/widget/scroll/mod.rs @@ -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 diff --git a/src/widget/scroll/tests.rs b/src/widget/scroll/tests.rs index 857eb8d..9edc0b8 100644 --- a/src/widget/scroll/tests.rs +++ b/src/widget/scroll/tests.rs @@ -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 diff --git a/src/widget/separator/mod.rs b/src/widget/separator/mod.rs index fc1fc34..cb3743c 100644 --- a/src/widget/separator/mod.rs +++ b/src/widget/separator/mod.rs @@ -142,5 +142,5 @@ impl From for Element } } -#[cfg(test)] +#[ cfg( test ) ] mod tests; diff --git a/src/widget/separator/tests.rs b/src/widget/separator/tests.rs index 1eca012..8fda5c4 100644 --- a/src/widget/separator/tests.rs +++ b/src/widget/separator/tests.rs @@ -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() ); diff --git a/src/widget/slider/tests.rs b/src/widget/slider/tests.rs index 88c5ce6..1d3c29a 100644 --- a/src/widget/slider/tests.rs +++ b/src/widget/slider/tests.rs @@ -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 }; diff --git a/src/widget/toggle/mod.rs b/src/widget/toggle/mod.rs index ddf74d6..7c8890f 100644 --- a/src/widget/toggle/mod.rs +++ b/src/widget/toggle/mod.rs @@ -7,7 +7,7 @@ use super::Element; mod theme; -#[cfg(test)] +#[ cfg( test ) ] mod tests; /// A two-state on / off switch. diff --git a/src/widget/toggle/tests.rs b/src/widget/toggle/tests.rs index 4a5121b..fb6161b 100644 --- a/src/widget/toggle/tests.rs +++ b/src/widget/toggle/tests.rs @@ -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 );