Files
ltk/tests/layout_stack_spacer.rs
Pedro M. de Echanove Pasquin ce893ac776
Some checks are pending
CI / build + test (push) Waiting to run
CI / cargo audit (push) Waiting to run
responsive fluid/physical scaling, widget-API stabilization, and perf guardrails
Responsive scaling. ltk now offers two first-class ways to size a UI so it adapts across screens, chosen per process via `WidgetScaling { Fluid, Physical }` (`set_widget_scaling` / `widget_scaling`, default `Fluid`). Fluid sizing (`Length::fluid( px )`) makes a design pixel a proportion of the surface's smaller side, calibrated against a reference width (`set_fluid_reference` / `fluid_reference`, 412 px default) and bounded by `FLUID_MIN` / `FLUID_MAX`; physical sizing (`Length::dp( px )`) is a constant-physical-size pixel scaled by display density (`set_density` / `density`). `Length` gains `orient( portrait, landscape )` — resolve one value in portrait, another in landscape — plus `widget( px )`, which picks fluid or dp per the active mode. Canvas exposes `geom_px` (geometry, resolved in physical layout space) and `font_px` (font size, bridging logical / physical per mode) so widgets and apps share one resolution path. Note the rename: `set_design_reference` / `design_reference` became `set_fluid_reference` / `fluid_reference`, and `Length::dp` changed meaning — the old surface-proportional behaviour now lives on `Length::fluid`.
Widgets. Every stock widget resolves its default geometry and font through the widget-scaling mode instead of frozen pixels, so a whole UI scales coherently without per-call units. New size builders where they were missing: `button` gains `font_size` / `height`, `text_edit` gains `height` / `font_size_fluid`, `separator` gains `pad_v`, and assorted widgets accept a `Length` where they previously took only `f32`.
Overlays. `OverlaySpec::size` is now `( Length, Length )` instead of `( u32, u32 )`, resolved against the main surface when the overlay is materialized, so overlays can scale with the display; `Length::px( … )` reproduces the old fixed sizing.
API stabilization (toward 1.0). Widget struct fields are now `pub( crate )` — they are configured through builders, not field access — except the value / state types apps genuinely read or construct (`Time`, `Date`, `ComboState`), which stay public. The internal `test_support` helpers move behind a `test-support` Cargo feature (off by default, so third-party builds never see them; ltk's own `make test` enables it). `Separator` drops its `0.0`-means-mode sentinel for `Option<Length>`, so an explicit `pad_v( 0.0 )` is a real flush divider distinct from the mode-following default.
Performance guardrails. Opt-in diagnostics via `LTK_PERF_WARN=1` warn about stuck animations, sustained software-render animation, and low `poll_interval`; software-rendered animation is capped near 30 Hz to spare CPU on machines that fall back off EGL. Apps can override the cap with `App::cap_software_animation`.
Docs and build. The two scaling modes are documented in README, onboarding and architecture, with the earlier gradient / backdrop doc drift cleaned up. The Makefile now ships the `locales/` directory into the packaged crate (fixing i18n keys rendering raw for downstreams), builds the new `responsive` example, and runs tests with `--features test-support`.
2026-07-07 17:40:33 +02:00

268 lines
8.7 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Integration coverage for `Stack` alignment / margin / translation and for
// the way `Spacer` distributes leftover space inside a `Column`.
use ltk::core::Canvas;
use ltk::{ stack, spacer, HAlign, VAlign, Rect };
fn make_canvas() -> Canvas
{
Canvas::new( 800, 600 )
}
fn rect( x: f32, y: f32, w: f32, h: f32 ) -> Rect
{
Rect { x, y, width: w, height: h }
}
// ── Stack: alignment ──────────────────────────────────────────────────────────
#[ test ]
fn stack_fill_child_takes_full_rect()
{
let canvas = make_canvas();
let s = stack::<()>().push( spacer().width( 100.0 ).height( 100.0 ) );
let layout = s.layout( rect( 0.0, 0.0, 400.0, 300.0 ), &canvas );
assert_eq!( layout.len(), 1 );
let ( child_rect, _ ) = layout[ 0 ];
// Fill ignores the spacer's preferred size and stretches across the
// entire stack rect.
assert_eq!( child_rect, rect( 0.0, 0.0, 400.0, 300.0 ) );
}
#[ test ]
fn stack_center_centers_child_inside_rect()
{
let canvas = make_canvas();
let child = spacer().width( 100.0 ).height( 50.0 );
let s = stack::<()>().push_aligned( child, HAlign::Center, VAlign::Center );
let layout = s.layout( rect( 0.0, 0.0, 400.0, 300.0 ), &canvas );
let ( child_rect, _ ) = layout[ 0 ];
assert_eq!( child_rect.x, 150.0 );
assert_eq!( child_rect.y, 125.0 );
assert_eq!( child_rect.width, 100.0 );
assert_eq!( child_rect.height, 50.0 );
}
#[ test ]
fn stack_end_bottom_anchors_to_bottom_right()
{
let canvas = make_canvas();
let child = spacer().width( 80.0 ).height( 40.0 );
let s = stack::<()>().push_aligned( child, HAlign::End, VAlign::Bottom );
let layout = s.layout( rect( 0.0, 0.0, 400.0, 300.0 ), &canvas );
let ( child_rect, _ ) = layout[ 0 ];
assert_eq!( child_rect.x, 320.0 );
assert_eq!( child_rect.y, 260.0 );
}
#[ test ]
fn stack_start_top_anchors_to_top_left()
{
let canvas = make_canvas();
let child = spacer().width( 40.0 ).height( 30.0 );
let s = stack::<()>().push_aligned( child, HAlign::Start, VAlign::Top );
let layout = s.layout( rect( 100.0, 50.0, 400.0, 300.0 ), &canvas );
let ( child_rect, _ ) = layout[ 0 ];
assert_eq!( child_rect.x, 100.0 );
assert_eq!( child_rect.y, 50.0 );
}
// ── Stack: margin ─────────────────────────────────────────────────────────────
#[ test ]
fn stack_margin_insets_fill_child_on_all_sides()
{
let canvas = make_canvas();
let child = spacer().width( 50.0 ).height( 50.0 );
let s = stack::<()>()
.push_aligned_margin( child, HAlign::Fill, VAlign::Fill, 12.0 );
let layout = s.layout( rect( 0.0, 0.0, 400.0, 300.0 ), &canvas );
let ( child_rect, _ ) = layout[ 0 ];
// Fill + margin = original rect inset on every side.
assert_eq!( child_rect, rect( 12.0, 12.0, 376.0, 276.0 ) );
}
#[ test ]
fn stack_margin_pushes_end_anchored_child_inward()
{
let canvas = make_canvas();
let child = spacer().width( 50.0 ).height( 50.0 );
let s = stack::<()>()
.push_aligned_margin( child, HAlign::End, VAlign::Top, 8.0 );
let layout = s.layout( rect( 0.0, 0.0, 400.0, 300.0 ), &canvas );
let ( child_rect, _ ) = layout[ 0 ];
// 400 - 50 - 8 = 342 from left edge; y starts at 8.
assert_eq!( child_rect.x, 342.0 );
assert_eq!( child_rect.y, 8.0 );
}
#[ test ]
fn stack_margin_larger_than_half_rect_clamps_inner_to_zero()
{
let canvas = make_canvas();
let child = spacer().width( 10.0 ).height( 10.0 );
let s = stack::<()>()
.push_aligned_margin( child, HAlign::Fill, VAlign::Fill, 200.0 );
// Stack rect is 100×100; margin 200 → inner is clamped to (0, 0).
let layout = s.layout( rect( 0.0, 0.0, 100.0, 100.0 ), &canvas );
let ( child_rect, _ ) = layout[ 0 ];
assert_eq!( child_rect.width, 0.0 );
assert_eq!( child_rect.height, 0.0 );
}
// ── Stack: translation ────────────────────────────────────────────────────────
#[ test ]
fn stack_translation_offsets_child_after_alignment()
{
let canvas = make_canvas();
let child = spacer().width( 40.0 ).height( 30.0 );
let s = stack::<()>()
.push_translated( child, HAlign::Start, VAlign::Top, 5.0, 7.0 );
let layout = s.layout( rect( 0.0, 0.0, 400.0, 300.0 ), &canvas );
let ( child_rect, _ ) = layout[ 0 ];
assert_eq!( child_rect.x, 5.0 );
assert_eq!( child_rect.y, 7.0 );
}
// ── Stack: order ─────────────────────────────────────────────────────────────
#[ test ]
fn stack_layout_preserves_child_order()
{
let canvas = make_canvas();
let s = stack::<()>()
.push( spacer().width( 10.0 ).height( 10.0 ) )
.push( spacer().width( 20.0 ).height( 20.0 ) )
.push( spacer().width( 30.0 ).height( 30.0 ) );
let layout = s.layout( rect( 0.0, 0.0, 400.0, 300.0 ), &canvas );
assert_eq!( layout.len(), 3 );
assert_eq!( layout[ 0 ].1, 0 );
assert_eq!( layout[ 1 ].1, 1 );
assert_eq!( layout[ 2 ].1, 2 );
}
// ── Stack: preferred_size ─────────────────────────────────────────────────────
#[ test ]
fn stack_preferred_size_returns_max_child_height()
{
let canvas = make_canvas();
let s = stack::<()>()
.push( spacer().width( 40.0 ).height( 30.0 ) )
.push( spacer().width( 60.0 ).height( 80.0 ) );
let ( w, h ) = s.preferred_size( 400.0, &canvas );
// Stack always reports `max_width` for width, and the tallest child for
// height — Stack is meant for overlaying, not flow.
assert_eq!( w, 400.0 );
assert_eq!( h, 80.0 );
}
#[ test ]
fn stack_empty_preferred_size_height_is_zero()
{
let canvas = make_canvas();
let s: ltk::Stack<()> = stack();
let ( _, h ) = s.preferred_size( 400.0, &canvas );
assert_eq!( h, 0.0 );
}
// ── Spacer: builder semantics ─────────────────────────────────────────────────
// Builder-field storage (weight / fixed dimensions) is covered by internal
// unit tests in `src/layout/spacer.rs`; here we exercise only the public
// observable — `preferred_size` — which reflects those fields.
#[ test ]
fn spacer_default_has_no_fixed_size()
{
let canvas = make_canvas();
assert_eq!( spacer().preferred_size( &canvas ), ( 0.0, 0.0 ) );
}
#[ test ]
fn spacer_height_pins_vertical_axis_only()
{
let canvas = make_canvas();
assert_eq!( spacer().height( 24.0 ).preferred_size( &canvas ), ( 0.0, 24.0 ) );
}
#[ test ]
fn spacer_width_pins_horizontal_axis_only()
{
let canvas = make_canvas();
assert_eq!( spacer().width( 12.0 ).preferred_size( &canvas ), ( 12.0, 0.0 ) );
}
// ── Spacer in Column: leftover distribution ───────────────────────────────────
// These use `column().layout(...)` directly (Column re-exports its `layout()`
// method as `pub`). Fixed-height children consume known space; flexible spacers
// must absorb the rest.
#[ test ]
fn flexible_spacer_in_column_consumes_leftover_height()
{
use ltk::column;
let canvas = make_canvas();
let col = column::<()>()
.padding( 0.0 )
.spacing( 0.0 )
.push( spacer().width( 50.0 ).height( 100.0 ) )
.push( spacer() ) // flexible
.push( spacer().width( 50.0 ).height( 100.0 ) );
let layout = col.layout( rect( 0.0, 0.0, 200.0, 500.0 ), &canvas );
assert_eq!( layout.len(), 3 );
// Two fixed children of 100 each + one spacer absorbing the leftover 300.
assert!( ( layout[ 0 ].0.height - 100.0 ).abs() < 1e-3 );
assert!( ( layout[ 1 ].0.height - 300.0 ).abs() < 1e-3 );
assert!( ( layout[ 2 ].0.height - 100.0 ).abs() < 1e-3 );
}
#[ test ]
fn weighted_spacers_split_leftover_proportionally()
{
use ltk::column;
let canvas = make_canvas();
let col = column::<()>()
.padding( 0.0 )
.spacing( 0.0 )
.push( spacer().weight( 1 ) )
.push( spacer().weight( 3 ) );
let layout = col.layout( rect( 0.0, 0.0, 200.0, 400.0 ), &canvas );
// Total weight 4; leftover 400; weight 1 → 100, weight 3 → 300.
assert!( ( layout[ 0 ].0.height - 100.0 ).abs() < 1e-3 );
assert!( ( layout[ 1 ].0.height - 300.0 ).abs() < 1e-3 );
}
#[ test ]
fn fixed_height_spacer_does_not_consume_leftover()
{
use ltk::column;
let canvas = make_canvas();
let col = column::<()>()
.padding( 0.0 )
.spacing( 0.0 )
.push( spacer().height( 50.0 ) ) // fixed — keeps its 50 px
.push( spacer() ); // flexible — absorbs the rest
let layout = col.layout( rect( 0.0, 0.0, 200.0, 500.0 ), &canvas );
assert!( ( layout[ 0 ].0.height - 50.0 ).abs() < 1e-3 );
assert!( ( layout[ 1 ].0.height - 450.0 ).abs() < 1e-3 );
}